Зачем использовать вложенные классы в C ++?

188

Может кто-нибудь указать мне на хорошие ресурсы для понимания и использования вложенных классов? У меня есть некоторые материалы, такие как Принципы программирования и такие вещи, как этот Центр знаний IBM - Вложенные классы

Но мне все еще трудно понять их цель. Может ли кто-нибудь помочь мне?

носящий очки
источник
15
Мой совет для вложенных классов в C ++ просто не использовать вложенные классы.
Билли ОНил
7
Они в точности как обычные классы ... кроме вложенных. Используйте их, когда внутренняя реализация класса настолько сложна, что ее легче всего смоделировать несколькими небольшими классами.
Meagar
12
@ Билли: почему? Кажется слишком широким для меня.
Джон Диблинг
30
Я до сих пор не видел аргумента, почему вложенные классы плохи по своей природе.
Джон Диблинг
7
@ 7vies: 1. потому что это просто не нужно - вы можете сделать то же самое с внешними классами, что уменьшает область действия любой заданной переменной, что хорошо. 2. потому что вы можете делать все, что могут делать вложенные классы typedef. 3. потому что они добавляют дополнительный уровень отступов в среде, где избежать длинных строк уже сложно 4. потому что вы объявляете два концептуально отдельных объекта в одной classдекларации и т. Д.
Billy ONeal

Ответы:

229

Вложенные классы хороши для сокрытия деталей реализации.

Список:

class List
{
    public:
        List(): head(nullptr), tail(nullptr) {}
    private:
        class Node
        {
              public:
                  int   data;
                  Node* next;
                  Node* prev;
        };
    private:
        Node*     head;
        Node*     tail;
};

Здесь я не хочу показывать Node, так как другие люди могут решить использовать класс, и это помешало бы мне обновить мой класс, поскольку все, что выставлено, является частью общедоступного API и должно поддерживаться вечно . Делая класс закрытым, я не только скрываю реализацию, но и говорю, что она моя, и я могу изменить ее в любое время, чтобы вы не могли ее использовать.

Посмотрите std::listили std::mapони все содержат скрытые классы (или они?). Дело в том, что они могут или не могут, но поскольку реализация является частной и скрытой, создатели STL смогли обновить код, не влияя на то, как вы использовали код, или оставив много старого багажа, лежащего вокруг STL, потому что им нужно поддерживать обратную совместимость с некоторыми дураками, которые решили использовать класс Node, который был спрятан внутри list.

Мартин Йорк
источник
9
Если вы делаете это, то Nodeне должны отображаться в заголовочном файле вообще.
Билли Онил
6
@Billy ONeal: Что, если я делаю реализацию файла заголовка, такую ​​как STL или boost?
Мартин Йорк,
6
@ Билли ONeal: Нет. Это вопрос хорошего дизайна, а не мнения. Помещение его в пространство имен не защищает его от использования. Теперь это часть общедоступного API, который нужно поддерживать вечно.
Мартин Йорк,
21
@Billy ONeal: защищает от случайного использования. Он также документирует тот факт, что он является частным и не должен использоваться (не может использоваться, если вы не делаете что-то глупое). Таким образом, вам не нужно поддерживать это. Помещение в пространство имен делает его частью общедоступного API (то, что вы упускаете в этом разговоре. Общедоступный API означает, что вам нужно его поддерживать).
Мартин Йорк,
10
@Billy ONeal: вложенный класс имеет некоторые преимущества перед вложенным пространством имен: вы не можете создавать экземпляры пространства имен, но вы можете создавать экземпляры класса. Что касается detailсоглашения: вместо этого, в зависимости от таких соглашений, нужно помнить о себе, лучше полагаться на компилятор, который отслеживает их для вас.
SasQ
142

Вложенные классы похожи на обычные классы, но:

  • они имеют дополнительное ограничение доступа (как и все определения внутри определения класса),
  • они не загрязняют данное пространство имен , например, глобальное пространство имен. Если вы чувствуете, что класс B так глубоко связан с классом A, но объекты A и B не обязательно связаны, то вы можете захотеть, чтобы класс B был доступен только через область видимости класса A (он будет называться A ::Класс).

Некоторые примеры:

Публично вложенный класс, чтобы поместить его в область видимости соответствующего класса


Предположим, вы хотите иметь класс, SomeSpecificCollectionкоторый будет агрегировать объекты класса Element. Вы можете затем либо:

  1. объявлять два класса: SomeSpecificCollectionи Element- плохо, потому что имя «Элемент» достаточно общее, чтобы вызвать возможное столкновение имен

  2. ввести пространство имен someSpecificCollectionи объявить классы someSpecificCollection::Collectionи someSpecificCollection::Element. Нет риска столкновения имен, но может ли оно стать более подробным?

  3. объявить два глобальных класса SomeSpecificCollectionи SomeSpecificCollectionElement- которые имеют незначительные недостатки, но, вероятно, в порядке.

  4. объявить глобальный класс SomeSpecificCollectionи класс Elementкак его вложенный класс. Затем:

    • вы не рискуете столкновением имен, так как Элемент не находится в глобальном пространстве имен,
    • в реализации SomeSpecificCollectionвы ссылаетесь просто Elementи везде SomeSpecificCollection::Element- как - + выглядит так же, как 3., но более понятно
    • становится просто, что это «элемент определенной коллекции», а не «конкретный элемент коллекции»
    • Видно, что SomeSpecificCollectionэто тоже класс.

На мой взгляд, последний вариант, безусловно, самый интуитивный и, следовательно, лучший дизайн.

Позвольте мне подчеркнуть - это не большая разница от создания двух глобальных классов с более подробными именами. Это просто крошечная деталь, но imho делает код более понятным.

Введение другой области видимости внутри класса


Это особенно полезно для введения typedefs или перечислений. Я просто выложу пример кода здесь:

class Product {
public:
    enum ProductType {
        FANCY, AWESOME, USEFUL
    };
    enum ProductBoxType {
        BOX, BAG, CRATE
    };
    Product(ProductType t, ProductBoxType b, String name);

    // the rest of the class: fields, methods
};

Один тогда позвонит:

Product p(Product::FANCY, Product::BOX);

Но при рассмотрении предложений по дополнению кода Product::часто можно получить список всех возможных значений перечисления (BOX, FANCY, CRATE), и здесь легко ошибиться (строго типизированные перечисления C ++ 0x решают эту проблему, но не берите в голову) ).

Но если вы введете дополнительную область видимости для этих перечислений, используя вложенные классы, вещи могут выглядеть следующим образом:

class Product {
public:
    struct ProductType {
        enum Enum { FANCY, AWESOME, USEFUL };
    };
    struct ProductBoxType {
        enum Enum { BOX, BAG, CRATE };
    };
    Product(ProductType::Enum t, ProductBoxType::Enum b, String name);

    // the rest of the class: fields, methods
};

Тогда звонок выглядит так:

Product p(Product::ProductType::FANCY, Product::ProductBoxType::BOX);

Затем, введя Product::ProductType::IDE, вы получите только перечисления из предложенной области действия. Это также снижает риск ошибки.

Конечно, это может не понадобиться для небольших классов, но если у вас много перечислений, то это облегчает работу клиентских программистов.

Таким же образом, вы можете «организовать» большую группу typedef в шаблоне, если вам когда-нибудь понадобится. Это полезный шаблон иногда.

Идиома PIMPL


PIMPL (сокращение от Pointer to IMPLementation) - это идиома, полезная для удаления деталей реализации класса из заголовка. Это уменьшает необходимость перекомпиляции классов в зависимости от заголовка класса всякий раз, когда изменяется часть «реализации» заголовка.

Обычно это реализуется с использованием вложенного класса:

Xh:

class X {
public:
    X();
    virtual ~X();
    void publicInterface();
    void publicInterface2();
private:
    struct Impl;
    std::unique_ptr<Impl> impl;
}

x.cpp:

#include "X.h"
#include <windows.h>

struct X::Impl {
    HWND hWnd; // this field is a part of the class, but no need to include windows.h in header
    // all private fields, methods go here

    void privateMethod(HWND wnd);
    void privateMethod();
};

X::X() : impl(new Impl()) {
    // ...
}

// and the rest of definitions go here

Это особенно полезно, если полное определение класса требует определения типов из некоторой внешней библиотеки, которая имеет тяжелый или просто уродливый заголовочный файл (например, WinAPI). Если вы используете PIMPL, тогда вы можете заключать любые специфичные для WinAPI функции только в .cppи никогда не включать их в .h.

Кос
источник
3
struct Impl; std::auto_ptr<Impl> impl; Эта ошибка была популяризирована Хербом Саттером. Не используйте auto_ptr для неполных типов или, по крайней мере, принимайте меры предосторожности, чтобы избежать генерирования неправильного кода.
Джин Бушуев
2
@Billy ONeal: Насколько мне известно, вы можете объявить auto_ptrнеполный тип в большинстве реализаций, но технически это UB в отличие от некоторых шаблонов в C ++ 0x (например unique_ptr), где было явно указано, что параметр шаблона может быть неполный тип и где именно тип должен быть завершен. (например, использование ~unique_ptr)
CB Bailey
2
@Billy ONeal: в C ++ 03 17.4.6.3 [lib.res.on.functions] говорит: «В частности, эффекты не определены в следующих случаях: [...] если в качестве аргумента шаблона используется неполный тип при создании экземпляра шаблона. " в то время как в C ++ 0x говорится «если неполный тип используется в качестве аргумента шаблона при создании экземпляра компонента шаблона, если это специально не разрешено для этого компонента». и позже (например): «Параметр шаблона Tиз unique_ptrможет быть неполным типа.»
CB Bailey
1
@MilesRout Это слишком общее. Зависит от того, разрешено ли наследовать клиентскому коду. Правило: если вы уверены, что не будете удалять через указатель базового класса, то виртуальный dtor полностью избыточен.
Кос
2
@IsaacPascual AWW, я должен обновить это сейчас, когда у нас есть enum class.
Кос,
21

Я не часто использую вложенные классы, но я использую их время от времени. Особенно, когда я определяю какой-то тип данных и затем хочу определить функтор STL, разработанный для этого типа данных.

Например, рассмотрим универсальный Fieldкласс, который имеет идентификационный номер, код типа и имя поля. Если я хочу выполнить поиск vectorэтих Fieldидентификаторов по номеру или имени, я мог бы создать функтор для этого:

class Field
{
public:
  unsigned id_;
  string name_;
  unsigned type_;

  class match : public std::unary_function<bool, Field>
  {
  public:
    match(const string& name) : name_(name), has_name_(true) {};
    match(unsigned id) : id_(id), has_id_(true) {};
    bool operator()(const Field& rhs) const
    {
      bool ret = true;
      if( ret && has_id_ ) ret = id_ == rhs.id_;
      if( ret && has_name_ ) ret = name_ == rhs.name_;
      return ret;
    };
    private:
      unsigned id_;
      bool has_id_;
      string name_;
      bool has_name_;
  };
};

Тогда код, который должен искать эти Fields, может использовать matchобласть действия внутри самого Fieldкласса:

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));
Джон Диблинг
источник
Спасибо за отличный пример и комментарии, хотя я не совсем знаком с функциями STL. Я заметил, что конструкторы в match () являются публичными. Я предполагаю, что конструкторы не всегда должны быть публичными, и в этом случае их нельзя создавать вне класса.
очках
1
@user: В случае функтора STL конструктор должен быть публичным.
Джон Диблинг
1
@ Билли: Я до сих пор не вижу каких-либо конкретных рассуждений, почему вложенные классы плохие.
Джон Диблинг
@John: Все руководящие принципы стиля кодирования сводятся к вопросу мнения. Я перечислил несколько причин в нескольких комментариях здесь, все из которых (на мой взгляд) являются разумными. Там нет «фактического» аргумента, который можно сделать, если код действителен и не вызывает неопределенного поведения. Тем не менее, я думаю, что приведенный здесь пример кода указывает на большую причину, по которой я избегаю вложенных классов, а именно - столкновения имен.
Билли Онил
1
Конечно, есть технические причины для предпочтения встроенных макросов !!
Майлз Рут
14

Можно реализовать шаблон Builder с вложенным классом . Особенно в C ++, лично я нахожу это семантически чище. Например:

class Product{
    public:
        class Builder;
}
class Product::Builder {
    // Builder Implementation
}

Скорее, чем:

class Product {}
class ProductBuilder {}
Ео
источник
Несомненно, это будет работать, если есть только одна сборка, но станет неприятным, если нужно иметь несколько сборщиков бетона. Надо тщательно принимать дизайнерские решения :)
irsis