Когда следует использовать частное наследование С++?

В отличие от защищенного наследования, собственное наследование С++ нашло свое отражение в основной разработке С++. Тем не менее, я до сих пор не нашел для этого хорошей пользы.

Когда вы его используете?

Ответ 1

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

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


ИЗМЕНИТЬ

В комментариях упоминалось, что это невозможно сделать родовым в то время, используя CRTP. Стандарт С++ 11 устраняет это ограничение, предоставляя другой синтаксис для поддержки аргументов шаблона:

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

Конечно, все это спорный вопрос, так как С++ 11 предоставляет final контекстная ключевое слово именно для этой цели:

class Sealed final // ...

Ответ 2

Я использую его все время. Несколько примеров от верхней части головы:

  • Когда я хочу выставить некоторый, но не весь интерфейс базового класса. Публичное наследование было бы ложью, поскольку Liskov substitutability нарушена, тогда как состав будет означать написание кучки функций пересылки.
  • Когда я хочу получить конкретный класс без виртуального деструктора. Открытое наследование приглашает клиентов удалять с помощью указателя на базу, вызывая поведение undefined.

Типичный пример получается в частном порядке из контейнера 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...  
};
  • При реализации шаблона адаптера наследование в частном порядке из класса Adapted позволяет пересылать вложенный экземпляр.
  • Для реализации частного интерфейса. Это часто возникает с помощью шаблона Observer. Обычно мой класс Observer, MyClass, подписывается на какой-то предмет. Тогда MyClass должен выполнить преобразование MyClass → Observer. Остальная часть системы не должна знать об этом, поэтому указано частное наследование.

Ответ 3

Каноническое использование частного наследования - это "реализовано в терминах" отношений (благодаря Скотту Майерсу "Эффективный С++" для этой формулировки). Другими словами, внешний интерфейс наследующего класса не имеет (видимого) отношения к унаследованному классу, но он использует его внутренне для реализации его функций.

Ответ 4

Я думаю, что критический раздел из С++ FAQ Lite:

Допустимое долгосрочное использование для частного наследования - это когда вы хотите построить класс Fred, который использует код в классе Wilma, а код из класса Wilma должен вызывать функции-члены из вашего нового класса Fred. В этом случае Фред называет не-виртуальных в Вилме, а Вильма сам по себе (обычно чистые виртуальные), которые переопределены Фредом. Это было бы намного сложнее с композицией.

Если вы сомневаетесь, вы должны предпочесть состав над частным наследованием.

Ответ 5

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

Например:

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, в то время как другие внешние классы не могут. Это отличный шаблон для обработки определенных обратных вызовов, которые определены как интерфейсы.

Ответ 6

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

[отредактировано в примере]

Возьмите пример, связанный с выше. Говоря, что

[...] классу Wilma необходимо вызвать функции-члены из вашего нового класса, Fred.

состоит в том, чтобы сказать, что Вильма требует, чтобы Фред мог ссылаться на определенные функции-члены, или, скорее, говорит, что Wilma является интерфейсом. Следовательно, как упоминалось в примере

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

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

[отредактировано в другом примере]

Эта страница вкратце обсуждает частные интерфейсы (с еще одного угла).

Ответ 7

Иногда мне кажется полезным использовать личное наследование, когда я хочу выставить меньший интерфейс (например, коллекцию) в интерфейсе другого, где реализация коллекции требует доступа к состоянию класса экспонирования, аналогично внутренние классы в 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). Нет необходимости иметь дополнительный элемент данных, занимающий место.

Ответ 8

Иногда это может быть альтернативой агрегации, например, если вы хотите агрегацию, но с измененным поведением агрегируемого объекта (переопределения виртуальных функций).

Но вы правы, у него мало примеров из реального мира.

Ответ 9

Если производный класс  - необходимо повторно использовать код и  - вы не можете изменить базовый класс и  - защищает свои методы, используя базовые элементы под замком.

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

Ответ 10

Частное наследование, которое будет использоваться, когда отношение не является "является", но новый класс может быть "реализован в терминах существующего класса" или новый класс "работает как существующий класс".

пример из "Стандартов кодирования С++ Андрея Александреску, Херба Саттера": - Учтите, что у двух классов Square и Rectangle есть виртуальные функции для установки их высоты и ширины. Тогда Square не может правильно наследовать Rectangle, потому что код, который использует модифицируемый Rectangle, будет предполагать, что SetWidth не изменяет высоту (независимо от того, явно ли Rectangle документирует контракт или нет), тогда как Square:: SetWidth не может сохранить этот контракт и свой собственный инвариант прямоугольности в в то же время. Но Rectangle также не может правильно наследовать от Square, если клиенты Square предполагают, например, что квадратная площадь равна квадрату ширины или если они полагаются на другое свойство, которое не выполняется для Rectangles.

Квадрат "is-a" прямоугольник (математически), но квадрат не является прямоугольником (поведенческим). Следовательно, вместо "is-a" мы предпочитаем говорить "works-like-a" (или, если хотите, "полезный-как-а" ), чтобы сделать описание менее склонным к недоразумению.

Ответ 11

Класс имеет инвариант. Инвариант устанавливается конструктором. Однако во многих ситуациях полезно иметь представление о состоянии представления объекта (которое вы можете передавать по сети или сохранять в файл - DTO, если хотите). REST лучше всего выполнять с точки зрения AggregateType. Это особенно верно, если вы являетесь константой. Рассмотрим:

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 не влияет на сумму); но когда нет кеша, стоит посмотреть его.

Частное наследование может быть почти всегда смоделировано членом (при необходимости, при необходимости ссылаясь на базу). Это просто не всегда стоит так моделировать; иногда наследование является наиболее эффективным представлением.

Ответ 12

Просто потому, что С++ имеет функцию, не означает, что она полезна или ее следует использовать.

Я бы сказал, вы не должны использовать его вообще.

Если вы все равно используете его, ну, вы в основном нарушаете инкапсуляцию и снижаете сплоченность. Вы помещаете данные в один класс и добавляете методы, которые манипулируют данными в другом.

Как и другие возможности С++, его можно использовать для достижения побочных эффектов, таких как герметизация класса (как упоминается в ответе dribeas), но это не делает его хорошей функцией.