Когда я должен использовать частное наследование C ++? - PullRequest
108 голосов
/ 18 марта 2009

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

Когда вы, ребята, используете это?

Ответы [ 13 ]

133 голосов
/ 24 марта 2009

Я использую это все время. Несколько примеров из головы:

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

Типичным примером является частное получение из контейнера 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. Остальная часть системы не должна знать об этом, поэтому указано частное наследование.
53 голосов
/ 18 марта 2009

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

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

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

21 голосов
/ 24 марта 2009

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

Например:

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

17 голосов
/ 18 марта 2009

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

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

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

4 голосов
/ 18 марта 2009

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

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

Возьмите пример , указанный выше. Сказать, что

[...] класс Вильме нужно вызвать функции-члены из вашего нового класса, Фред.

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

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

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

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

На этой странице кратко обсуждаются частные интерфейсы (с другой стороны).

2 голосов
/ 25 марта 2009

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

1 голос
/ 30 апреля 2018

Я нашел хорошее приложение для частного наследования, хотя оно имеет ограниченное использование.

Проблема, которую нужно решить

Предположим, вы получили следующий C 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 голос
/ 19 апреля 2011

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

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

1 голос
/ 18 марта 2009

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

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

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