Коли я повинен використовувати приватне успадкування C ++?


116

На відміну від захищеного успадкування, C ++ приватне успадкування знайшло свій шлях у розвиток основного C ++. Однак я все ще не знайшов для цього гарного використання.

Коли ви, хлопці, користуєтесь ним?

c++  oop 

Відповіді:


60

Примітка після прийняття відповіді: це НЕ повна відповідь. Прочитайте інші відповіді як тут (концептуально), так і тут (як теоретичні, так і практичні), якщо вас цікавить питання. Це просто химерна хитрість, яку можна досягти приватним успадкуванням. Хоча це фантазія, це не відповідь на питання.

Окрім основного використання лише приватного успадкування, показаного у FAQ + на C ++ (зв'язаний у коментарях інших), ви можете використовувати комбінацію приватного та віртуального успадкування для герметизації класу (у .NET термінології) або для складання класу остаточним (у термінології Java) . Це не звичайне використання, але все одно мені було цікаво:

class ClassSealer {
private:
   friend class Sealed;
   ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{ 
   // ...
};
class FailsToDerive : public Sealed
{
   // Cannot be instantiated
};

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

FailsToDerive не компілюватиметься, оскільки він повинен викликати конструктор ClassSealer безпосередньо (вимога віртуального спадкування), але він не може бути приватним у класі Sealed, і в цьому випадку FailsToDerive не є другом ClassSealer .


EDIT

У коментарях зазначалося, що в цей час це неможливо зробити загальним за допомогою CRTP. Стандарт C ++ 11 знімає це обмеження, надаючи інший синтаксис для аргументів шаблону дружби:

template <typename T>
class Seal {
   friend T;          // not: friend class T!!!
   Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...

Звичайно, це все спірно, оскільки C ++ 11 надає finalконтекстуальне ключове слово саме для цієї мети:

class Sealed final // ...

Це чудова техніка. Я напишу на ньому запис у блозі.

1
Питання: якби ми не використовували віртуальне успадкування, тоді FailsToDerive збирається. Правильно?

4
+1. @Sasha: Правильне віртуальне успадкування потрібне, оскільки найпохідніший клас завжди викликає безпосередньо конструкторів усіх практично успадкованих класів, що не буває у випадку звичайного успадкування.
j_random_hacker

5
Це можна зробити загальним, не створюючи спеціального ClassSealer для кожного класу, який ви хочете запечатати! Перевірте це: class ClassSealer {захищено: ClassSealer () {}}; це все.

+1 Iraimbilanja, дуже круто! До речі, я бачив ваш попередній коментар (тепер видалений) щодо використання CRTP: я думаю, що насправді має працювати, просто складно правильно встановити синтаксис для друзів з шаблонами. Але в будь-якому випадку ваше не шаблонне рішення набагато приголомшливіше
j_random_hacker

138

Я ним користуюся постійно. Кілька прикладів з моєї голови:

  • Коли я хочу розкрити деякий, але не весь інтерфейс базового класу. Публічне успадкування було б брехнею, оскільки порушена замінюваність Ліскова , тоді як композиція означатиме написання купу функцій переадресації.
  • Коли я хочу вийти з конкретного класу без віртуального деструктора. Публічне успадкування запрошує клієнтів видалити через покажчик на базу, посилаючись на невизначене поведінку.

Типовий приклад - приватне отримання контейнера STL:

class MyVector : private vector<int>
{
public:
    // Using declarations expose the few functions my clients need 
    // without a load of forwarding functions. 
    using vector<int>::push_back;
    // etc...  
};
  • При впровадженні шаблону адаптера спадкування приватно від класу Адаптований економить необхідність переадресації до вкладеного екземпляра.
  • Для реалізації приватного інтерфейсу. Це часто зустрічається із шаблоном спостерігачів. Як правило, мій клас Observer, кажуть MyClass, підписується на якийсь предмет. Тоді лише MyClass потребує перетворення MyClass -> Observer. Решта системи не повинні про це знати, тому вказується приватне успадкування.

4
@Krsna: Власне, я не думаю. Тут є лише одна причина: лінь, окрім останньої, яка була б складнішою для роботи.
Матьє М.

11
Не так багато лінощів (якщо ви не маєте на увазі це по-доброму). Це дозволяє створювати нові перевантаження функцій, які були викриті без зайвих робіт. Якщо в C ++ 1x вони додають 3 нові перевантаження push_back, MyVectorотримують їх безкоштовно.
Девід Стоун

@DavidStone, ви не можете це зробити методом шаблону?
Julien__

5
@Julien__: Так, ви можете писати template<typename... Args> constexpr decltype(auto) f(Args && ... args) noexcept(noexcept(std::declval<Base &>().f(std::forward<Args>(args)...)) and std::is_nothrow_move_constructible<decltype(std::declval<Base &>().f(std::forward<Args>(args)...))>) { return m_base.f(std::forward<Args>(args)...); }або писати, використовуючи Base::f;. Якщо ви хочете отримати більшу частину функціональності та гнучкості, яку надає приватне успадкування та usingзаява, у вас є ця чудовиська для кожної функції (і не забувайте про constі volatileперевантажуйте!)
Девід Стоун

2
Я кажу, що більшість функціональних можливостей є тим, що ви все ще викликаєте один додатковий конструктор переміщення, якого немає у використаній версії оператора. Взагалі, ви б очікували, що це буде оптимізовано, але ця функція теоретично може повертати непересувний тип за значенням. Шаблон функції переадресації також має додаткову інстанціювання шаблону та глибину конспекту. Це може призвести до обмеження вашої програми обмеженням.
Девід Стоун

31

Канонічне використання приватного успадкування - це "реалізовані з точки зору" відносини (завдяки "ефективному С ++" Скотта Майєрса для цієї редакції). Іншими словами, зовнішній інтерфейс класу успадкування не має (видимого) відношення до спадкового класу, але він використовує його внутрішньо для реалізації його функціональності.


6
Можливо, варто згадати одну з причин, чому він використовується в цьому випадку: Це дозволяє проводити оптимізацію порожнього базового класу, яка не відбудеться, якщо клас був членом замість базового класу.
джельф

2
його основне використання полягає у зменшенні споживання простору там, де це дійсно має значення, наприклад, у класах рядків, керованих політикою, або у стислих парах. власне, boost :: compression_pair використовував захищене успадкування.
Йоханнес Шауб - ліб

jalf: Гей, я цього не усвідомлював. Я вважав, що непублічне успадкування в основному використовується як хак, коли вам потрібен доступ до захищених членів класу. Мені цікаво, чому порожній об’єкт займає будь-який простір при використанні композиції. Можливо, для універсальної адресації ...

3
Це також зручно зробити клас, який не можна скопіювати - просто приватно успадковуйте його з порожнього класу, який не можна скопіювати. Тепер вам не доведеться проходити через напружену роботу щодо декларування, але не визначення приватного конструктора копій та оператора призначення. Меєрс теж говорить про це.
Майкл Берр

Я не усвідомлював, що це питання стосується приватного спадкування замість захищеного спадкування. так, я гадаю, для цього є досить багато додатків. Не можу придумати багатьох прикладів захищеного успадкування: / виглядає так, що корисно лише рідко.
Йоханнес Шауб - ліб

23

Одним корисним використанням приватного успадкування є те, коли у вас є клас, який реалізує інтерфейс, який потім реєструється з яким-небудь іншим об'єктом. Ви робите цей інтерфейс приватним, так що сам клас повинен реєструватися, і лише ті конкретні об'єкти, з якими він зареєстрований, можуть використовувати ці функції.

Наприклад:

class FooInterface
{
public:
    virtual void DoSomething() = 0;
};

class FooUser
{
public:
    bool RegisterFooInterface(FooInterface* aInterface);
};

class FooImplementer : private FooInterface
{
public:
    explicit FooImplementer(FooUser& aUser)
    {
        aUser.RegisterFooInterface(this);
    }
private:
    virtual void DoSomething() { ... }
};

Тому клас FooUser може викликати приватні методи FooImplementer через інтерфейс FooInterface, тоді як інші зовнішні класи не можуть. Це чудова модель для обробки конкретних зворотних викликів, визначених як інтерфейси.


1
Дійсно, приватне успадкування є приватним IS-A.
curiousguy

18

Я думаю, що найважливішим розділом у програмі C ++ FAQ Lite є:

Законним, довготерміновим використанням для приватного успадкування є те, коли ви хочете створити клас Фред, який використовує код у класі Вільми, а код класу Вільма повинен викликати функції члена з вашого нового класу, Фред. У цьому випадку Фред називає невіртуальними у Вільмі, а Вільма називає (як правило, чистими віртуалами) сам по собі, які переосмислюються Фредом. З композицією це було б набагато складніше.

Якщо ви сумніваєтесь, вам слід віддати перевагу композиції над приватною спадщиною.


4

Мені здається корисним для інтерфейсів (а саме абстрактних класів), які я успадковую там, де не хочу, щоб інший код торкався інтерфейсу (лише клас успадкування).

[відредаговано в прикладі]

Візьміть приклад, пов'язаний вище. Кажучи це

[...] Клас Вільма повинен викликати функції члена з вашого нового класу, Фред.

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

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

коментарі щодо бажаного ефекту від програмістів, які потребують задоволення наших вимог до інтерфейсу, або порушення коду. І, оскільки fredCallsWilma () захищений, лише друзі, а похідні класи можуть торкатися його, тобто спадковий інтерфейс (абстрактний клас), до якого може торкатися лише клас спадкування (та друзі).

[відредаговано в іншому прикладі]

На цій сторінці коротко обговорюються приватні інтерфейси (з іншого боку).


Насправді це не

Я думаю, я бачу, куди ти йдеш ... Типовим випадком використання може бути те, що Wilma - це якийсь клас утиліти, якому потрібно викликати віртуальні функції у Фреда, але інші класи не повинні знати, що Fred реалізовано в термінах, Вільми. Правильно?
j_random_hacker

Так. Я мушу зазначити, що, наскільки я розумію, в Java частіше використовується термін «інтерфейс». Коли я вперше почув це, я подумав, що це міг би дати кращу назву. Оскільки в цьому прикладі у нас є інтерфейс, до якого ніхто не інтерфейсує так, як ми зазвичай думаємо про це слово.
упередженість

@Noos: Так, я думаю, що ваше твердження "Вільма - це інтерфейс" є дещо неоднозначним, оскільки більшість людей вважають, що Вільма - це інтерфейс, який Фред має намір поставити у світ , а не контракт лише з Вільмою.
j_random_hacker

@j_ Тому я вважаю, що інтерфейс - це невірне ім'я. Інтерфейс, термін, не повинен означати світ, як можна було б подумати, а скоріше, це гарантія функціональності. Власне, я був суперечливим щодо терміна інтерфейсу в моєму класі програмування. Але ми використовуємо те, що нам дано ...
упередження

2

Іноді мені здається корисним використовувати приватне успадкування, коли я хочу відкрити менший інтерфейс (наприклад, колекцію) в інтерфейсі іншого, коли реалізація колекції вимагає доступу до стану класу, що відкриває, аналогічно внутрішнім класам у Java.

class BigClass;

struct SomeCollection
{
    iterator begin();
    iterator end();
};

class BigClass : private SomeCollection
{
    friend struct SomeCollection;
    SomeCollection &GetThings() { return *this; }
};

Тоді якщо SomeCollection потребує доступу до BigClass, він може static_cast<BigClass *>(this). Не потрібно мати зайвого члена даних, який займає місце.


У BigClassцьому прикладі немає необхідності в прямому оголошенні ? Мені це здається цікавим, але це кричить хакіт в моє обличчя.
Томас Едінг

2

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

Проблема для вирішення

Припустимо, вам надано наступний API API:

#ifdef __cplusplus
extern "C" {
#endif

    typedef struct
    {
        /* raw owning pointer, it's C after all */
        char const * name;

        /* more variables that need resources
         * ...
         */
    } Widget;

    Widget const * loadWidget();

    void freeWidget(Widget const * widget);

#ifdef __cplusplus
} // end of extern "C"
#endif

Тепер ваше завдання - реалізувати цей API за допомогою C ++.

C-ish підхід

Звичайно, ми можемо вибрати такий стиль реалізації C-ish:

Widget const * loadWidget()
{
    auto result = std::make_unique<Widget>();
    result->name = strdup("The Widget name");
    // More similar assignments here
    return result.release();
}

void freeWidget(Widget const * const widget)
{
    free(result->name);
    // More similar manual freeing of resources
    delete widget;
}

Але є кілька недоліків:

  • Ручне управління ресурсами (наприклад, пам'ять)
  • Налаштувати struct неправильно
  • Легко забути звільнити ресурси при звільненні struct
  • Це C-ish

C ++ підхід

Нам дозволено використовувати C ++, то чому б не використати його повноцінні повноваження?

Впровадження автоматизованого управління ресурсами

Зазначені вище проблеми в основному пов'язані з ручним управлінням ресурсами. Рішення, яке спадає на думку, полягає у успадкуванні Widgetта доданні екземпляра управління ресурсами до похідного класу WidgetImplдля кожної змінної:

class WidgetImpl : public Widget
{
public:
    // Added bonus, Widget's members get default initialized
    WidgetImpl()
        : Widget()
    {}

    void setName(std::string newName)
    {
        m_nameResource = std::move(newName);
        name = m_nameResource.c_str();
    }

    // More similar setters to follow

private:
    std::string m_nameResource;
};

Це спрощує реалізацію до наступного:

Widget const * loadWidget()
{
    auto result = std::make_unique<WidgetImpl>();
    result->setName("The Widget name");
    // More similar setters here
    return result.release();
}

void freeWidget(Widget const * const widget)
{
    // No virtual destructor in the base class, thus static_cast must be used
    delete static_cast<WidgetImpl const *>(widget);
}

Таким чином ми усунули всі вищезазначені проблеми. Але клієнт все ще може забути про сетерів WidgetImplта призначити їхWidget членів безпосередньо.

Приватне успадкування виходить на сцену

Для інкапсуляції Widgetчленів ми використовуємо приватне успадкування. На жаль, зараз нам потрібні дві додаткові функції для передачі між обома класами:

class WidgetImpl : private Widget
{
public:
    WidgetImpl()
        : Widget()
    {}

    void setName(std::string newName)
    {
        m_nameResource = std::move(newName);
        name = m_nameResource.c_str();
    }

    // More similar setters to follow

    Widget const * toWidget() const
    {
        return static_cast<Widget const *>(this);
    }

    static void deleteWidget(Widget const * const widget)
    {
        delete static_cast<WidgetImpl const *>(widget);
    }

private:
    std::string m_nameResource;
};

Для цього необхідні наступні адаптації:

Widget const * loadWidget()
{
    auto widgetImpl = std::make_unique<WidgetImpl>();
    widgetImpl->setName("The Widget name");
    // More similar setters here
    auto const result = widgetImpl->toWidget();
    widgetImpl.release();
    return result;
}

void freeWidget(Widget const * const widget)
{
    WidgetImpl::deleteWidget(widget);
}

Це рішення вирішує всі проблеми. Немає керування пам'яттю вручну і Widgetдобре інкапсульовано, так щоWidgetImpl більше не було членів публічних даних. Це робить програму простою для правильного використання та важкою (неможливою?) Неправильною.

Фрагменти коду є прикладом компіляції на Coliru .


1

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

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


1

Іноді це може бути альтернативою агрегації , наприклад, якщо ви хочете агрегування, але зі зміненою поведінкою об'єднаної сутності (переосмислення віртуальних функцій).

Але ти маєш рацію, у неї не так багато прикладів із реального світу.


0

Приватне успадкування, яке слід використовувати, коли відношення не є "є", але Новий клас можна "реалізувати в терміні існуючого класу" або новий клас "працює як" існуючий клас.

Приклад з "C ++ стандартів кодування Андрія Олександреску, Herb Sutter": - Врахуйте, що два класи Square і Rectangle мають віртуальні функції для встановлення їх висоти та ширини. Тоді Square не може правильно успадкувати прямокутник, тому що код, який використовує модифікований Прямокутник, передбачає, що SetWidth не змінює висоту (чи прямокутний прямокутник документує контракт, чи ні), тоді як Square :: SetWidth не може зберегти цей контракт та власну інваріантність квадратів у одночасно. Але Прямокутник також не може правильно успадкувати Квадрат, якщо клієнти Площі припускають, наприклад, що площа Площі має ширину у квадраті, або якщо вони покладаються на якусь іншу властивість, яка не відповідає правилам Прямокутників.

Квадрат прямокутника "є-а" (математично), але квадрат - це не прямокутник (поведінковий). Отже, замість "є-а", ми вважаємо за краще сказати "працює-як-а" (або, якщо ви віддаєте перевагу, "корисний-як-а"), щоб зробити опис менш схильним до непорозумінь.


0

Клас має інваріант. Інваріант встановлюється конструктором. Однак у багатьох ситуаціях корисно мати перегляд стану представлення об'єкта (який ви можете передати через мережу або зберегти у файл - DTO, якщо хочете). REST найкраще проводити з точки зору агрегатного типу. Це особливо вірно, якщо ти правильний. Поміркуйте:

struct QuadraticEquationState {
   const double a;
   const double b;
   const double c;

   // named ctors so aggregate construction is available,
   // which is the default usage pattern
   // add your favourite ctors - throwing, try, cps
   static QuadraticEquationState read(std::istream& is);
   static std::optional<QuadraticEquationState> try_read(std::istream& is);

   template<typename Then, typename Else>
   static std::common_type<
             decltype(std::declval<Then>()(std::declval<QuadraticEquationState>()),
             decltype(std::declval<Else>()())>::type // this is just then(qes) or els(qes)
   if_read(std::istream& is, Then then, Else els);
};

// this works with QuadraticEquation as well by default
std::ostream& operator<<(std::ostream& os, const QuadraticEquationState& qes);

// no operator>> as we're const correct.
// we _might_ (not necessarily want) operator>> for optional<qes>
std::istream& operator>>(std::istream& is, std::optional<QuadraticEquationState>);

struct QuadraticEquationCache {
   mutable std::optional<double> determinant_cache;
   mutable std::optional<double> x1_cache;
   mutable std::optional<double> x2_cache;
   mutable std::optional<double> sum_of_x12_cache;
};

class QuadraticEquation : public QuadraticEquationState, // private if base is non-const
                          private QuadraticEquationCache {
public:
   QuadraticEquation(QuadraticEquationState); // in general, might throw
   QuadraticEquation(const double a, const double b, const double c);
   QuadraticEquation(const std::string& str);
   QuadraticEquation(const ExpressionTree& str); // might throw
}

На даний момент ви можете просто зберігати колекції кешу в контейнерах і шукати його на побудові. Зручно, якщо є якась реальна обробка. Зауважте, що кеш є частиною QE: операції, визначені на QE, можуть означати, що кеш може бути частково використаний (наприклад, c не впливає на суму); все ж, коли немає кешу, варто переглянути його.

Приватне успадкування може майже завжди моделюватися членом (зберігаючи посилання на базу, якщо потрібно). Просто не завжди варто так моделювати; іноді успадкування є найбільш ефективним представництвом.


0

Якщо вам потрібні std::ostreamневеликі зміни (як у цьому питанні ), можливо, вам знадобиться

  1. Створіть клас, MyStreambufякий походить відstd::streambuf та внесете зміни там
  2. Створіть клас, MyOStreamщо випливає з std::ostreamцього, також ініціалізує та керує екземпляром MyStreambufта передає вказівник на цей екземпляр конструкторуstd::ostream

Першою ідеєю може бути додавання MyStreamекземпляра як члена даних до MyOStreamкласу:

class MyOStream : public std::ostream
{
public:
    MyOStream()
        : std::basic_ostream{ &m_buf }
        , m_buf{}
    {}

private:
    MyStreambuf m_buf;
};

Але базові класи будуються перед будь-якими членами даних, тому ви передаєте вказівник на ще не побудований std::streambufекземпляр, на std::ostreamякий є невизначена поведінка.

Рішення пропонується у відповіді Бена на вищезазначене питання , просто успадкуйте спочатку буфер потоку, потім від потоку, а потім ініціалізуйте потік за допомогою this:

class MyOStream : public MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};

Однак отриманий клас також може бути використаний як std::streambufекземпляр, який зазвичай небажаний. Перехід до приватного успадкування вирішує цю проблему:

class MyOStream : private MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};

-1

Тільки тому, що C ++ має особливість, не означає, що вона корисна або що її слід використовувати.

Я б сказав, що ви взагалі не повинні його використовувати.

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

Як і інші функції C ++, його можна використовувати для досягнення побічних ефектів, таких як герметизація класу (як згадується у відповіді Дрибея), але це не робить його гарною особливістю.


ти саркастичний? все, що у мене є, -1! все одно я не
видаляю

9
" ти в основному порушуєш інкапсуляцію ". Можна привести приклад?
curiousguy

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