Чому можна використовувати вкладені класи в C ++?


188

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

Але у мене все ще виникають проблеми з розумінням їхнього призначення. Може хтось, будь ласка, допоможе мені?


15
Моя порада для вкладених класів у C ++ - це просто не використовувати вкладені класи.
Біллі ONeal

7
Вони точно схожі на звичайні заняття ... крім вкладених. Використовуйте їх, коли внутрішня реалізація класу настільки складна, що її найпростіше змоделювати декілька менших класів.
meagar

12
@Billy: Чому? Мені здається занадто широким.
Джон Дайблінг

30
Я досі не бачив аргументу, чому вкладені класи погані за своєю природою.
Джон Дайблінг

7
@ 7vies: 1. тому що це просто не потрібно - ви можете зробити те ж саме із зовнішньо визначеними класами, що зменшує область застосування будь-якої заданої змінної, що добре. 2. тому що ви можете робити все, що можна зробити з вкладених класів typedef. 3. тому що вони додають додатковий рівень відступу в середовищі, коли уникнути довгих рядків вже важко 4. тому що ви оголошуєте два концептуально окремих об’єкти в одній classдекларації тощо.
Біллі 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взагалі не слід виявляти у файлі заголовка.
Біллі ONeal

6
@Billy ONeal: Що робити, якщо я роблю реалізацію файлу заголовка, наприклад, STL або boost.
Мартін Йорк

6
@Billy ONeal: Ні. Це питання хорошого дизайну, а не думки. Якщо розмістити його в просторі імен, це не захистить його від використання. Зараз це частина публічного API, яку потрібно підтримувати на вічність.
Мартін Йорк

21
@Billy ONeal: захищає його від випадкового використання. Він також документує той факт, що він приватний і його не слід використовувати (не можна використовувати, якщо ви не зробите щось дурне). Таким чином, вам не потрібно його підтримувати. Якщо розмістити його в просторі імен, він стає частиною загальнодоступного API (те, що вам не вистачає в цій розмові. Public 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це теж клас.

На мою думку, останній варіант, безумовно, найбільш інтуїтивний і, отже, найкращий дизайн.

Дозвольте наголосити - це не велика різниця від створення двох глобальних класів з більш багатослівними іменами. Це просто крихітна деталь, але їм це робить код більш зрозумілим.

Введення іншої області в межах класу


Це особливо корисно для введення typedefs або enums. Я просто опублікую тут приклад коду:

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, ви отримаєте лише перерахунки з потрібної запропонованої області. Це також зменшує ризик помилки.

Звичайно, це може не знадобитися для невеликих занять, але якщо у вас є багато перераховань, то це полегшує роботу клієнтських програмістів.

Таким же чином, ви могли б "організувати" велику купу типів у шаблоні, якщо у вас коли-небудь виникне потреба. Іноді це корисна модель.

Ідіома PIMPL


PIMPL (скорочення покажчика на 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цих Fields за ідентифікаційним номером або іменем, я можу побудувати функтор для цього:

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
@Billy: Я ще не бачив конкретних міркувань, чому вкладені класи погані.
Джон Дайблінг

@John: Усі рекомендації щодо стилю кодування зводяться до питання думки. Я перерахував декілька причин у кількох коментарях навколо, і всі вони (на мій погляд) є розумними. Немає "фактичного" аргументу, який можна зробити, якщо код дійсний і не викликає невизначеної поведінки. Однак, я думаю, що наведений тут приклад коду вказує на велику причину, чому я уникаю вкладених класів, а саме - зіткнення імен.
Біллі ONeal

1
Звичайно, є технічні причини віддавати перевагу інліній макросам !!
Miles Rout

14

Можна реалізувати шаблон Builder з вкладеним класом . Особливо в C ++, особисто я вважаю це семантично чистішим. Наприклад:

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

Замість:

class Product {}
class ProductBuilder {}

Звичайно, це спрацює, якщо буде лише одна конструкція, але буде неприємно, якщо буде потреба мати декількох будівельників з бетону. Слід ретельно приймати дизайнерські рішення :)
irsis
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.