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

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

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

Ответ 1

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

Список:

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

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

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

Ответ 2

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

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

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

Публично объявить класс в класс соответствующего класса


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

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

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

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

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

    • вы не рискуете столкновениями имен, поскольку 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), и здесь легко сделать ошибку (С++ 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, вы получите только перечисления из требуемой области действия. Это также снижает риск совершения ошибки.

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

Точно так же вы можете "организовать" большой набор typedefs в шаблоне, если вам когда-либо понадобится. Иногда это полезная модель.

Идиома 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

Я не использую вложенные классы много, но я использую их время от времени. Особенно, когда я определяю какой-то тип данных, и тогда я хочу определить функтор 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_;
  };
};

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

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));