Зачем использовать вложенные классы в C ++? - PullRequest
170 голосов
/ 31 декабря 2010

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

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

Ответы [ 4 ]

206 голосов
/ 31 декабря 2010

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

Список:

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.

137 голосов
/ 31 декабря 2010

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

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

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

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


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

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

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

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

  4. объявляет глобальный класс SomeSpecificCollection и класс 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), и здесь легко ошибиться (строго типизированные перечисления 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) - это идиома, полезная для удаления деталей реализации класса из заголовка. Это уменьшает необходимость перекомпиляции классов в зависимости от заголовка класса, когда изменяется часть «реализации» заголовка.

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

X.h:

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.

21 голосов
/ 31 декабря 2010

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

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

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 s может использовать match в пределах класса Field:

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));
12 голосов
/ 27 сентября 2015

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

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

Вместо:

class Product {}
class ProductBuilder {}
...