В отличие от защищенного наследования, частное наследование C ++ нашло свое отражение в основных разработках C ++. Однако я до сих пор не нашел ему хорошего применения.
Когда вы, ребята, этим пользуетесь?
Примечание после принятия ответа: это НЕ полный ответ. Прочтите другие ответы, такие как здесь (концептуально) и здесь (как теоретические, так и практические), если вас интересует вопрос. Это просто причудливый трюк, которого можно достичь с помощью частного наследования. В то время как фантазии это не ответ на вопрос.
Помимо базового использования только частного наследования, показанного в C ++ 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. Стандарт C ++ 11 снимает это ограничение, предоставляя другой синтаксис для поддержки аргументов шаблона:
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
Конечно, это все спорный вопрос, поскольку C ++ 11 предоставляет final
ключевое слово contextual именно для этой цели:
class Sealed final // ...
Я использую это все время. Несколько примеров из моей головы:
Типичный пример - частное извлечение из контейнера STL:
источник
push_back
,MyVector
получит их бесплатно.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
перегрузках!).Каноническое использование частного наследования - это отношение «реализовано в терминах» (спасибо Скотту Мейерсу «Эффективный C ++» за эту формулировку). Другими словами, внешний интерфейс наследующего класса не имеет (видимого) отношения к унаследованному классу, но использует его внутри для реализации своих функций.
источник
Одно из полезных применений частного наследования - это когда у вас есть класс, реализующий интерфейс, который затем регистрируется с другим объектом. Вы делаете этот интерфейс закрытым, так что сам класс должен регистрироваться, и только конкретный объект, с которым он зарегистрирован, может использовать эти функции.
Например:
Поэтому класс FooUser может вызывать частные методы FooImplementer через интерфейс FooInterface, в то время как другие внешние классы не могут. Это отличный шаблон для обработки определенных обратных вызовов, которые определены как интерфейсы.
источник
Я думаю, что критический раздел из C ++ FAQ Lite :
В случае сомнений вам следует предпочесть композицию частному наследованию.
источник
Я считаю это полезным для интерфейсов (а именно абстрактных классов), которые я наследую, когда я не хочу, чтобы другой код касался интерфейса (только наследующий класс).
[отредактировано в примере]
Возьмите пример, приведенный выше. Говоря это
означает, что Вильма требует, чтобы Фред мог вызывать определенные функции-члены, или, скорее, он говорит, что Вильма - это интерфейс . Следовательно, как указано в примере
комментирует желаемый эффект от программистов, которые должны соответствовать нашим требованиям к интерфейсу или нарушать код. И, поскольку fredCallsWilma () защищен, только друзья и производные классы могут касаться его, то есть унаследованного интерфейса (абстрактного класса), который может касаться только наследующий класс (и друзья).
[отредактировано в другом примере]
На этой странице кратко обсуждаются частные интерфейсы (еще под другим углом).
источник
Иногда мне кажется полезным использовать частное наследование, когда я хочу предоставить меньший интерфейс (например, коллекцию) в интерфейсе другого, где реализация коллекции требует доступа к состоянию выставляющего класса аналогично внутренним классам в Ява.
Затем, если SomeCollection потребуется доступ к BigClass, он сможет
static_cast<BigClass *>(this)
. Нет необходимости иметь дополнительный элемент данных, занимающий место.источник
BigClass
есть ли в этом примере? Мне это интересно, но это кричит мне в лицо.Я нашел хорошее приложение для частного наследования, хотя его использование ограничено.
Проблема для решения
Предположим, вам предоставлен следующий C API:
Теперь ваша задача - реализовать этот API с помощью C ++.
C-иш подход
Конечно, мы могли бы выбрать такой стиль реализации C-ish:
Но есть несколько недостатков:
struct
неправильныйstruct
Подход C ++
Нам разрешено использовать C ++, так почему бы не использовать его все возможности?
Внедрение автоматизированного управления ресурсами
Вышеупомянутые проблемы в основном связаны с ручным управлением ресурсами. На ум приходит решение наследовать от
Widget
производного класса и добавить экземпляр управления ресурсамиWidgetImpl
для каждой переменной:Это упрощает реализацию до следующего:
Таким образом мы устранили все вышеперечисленные проблемы. Но клиент все равно может забыть об установщиках
WidgetImpl
иWidget
напрямую назначить участников.На сцену выходит частное наследование
Для инкапсуляции
Widget
членов мы используем частное наследование. К сожалению, теперь нам нужны две дополнительные функции для преобразования между обоими классами:Это требует следующих адаптаций:
Это решение решает все проблемы. Нет ручного управления памятью и
Widget
красиво инкапсулирован, так чтоWidgetImpl
больше не имеет публичных членов данных. Это делает реализацию простой в использовании правильно и трудно (невозможно?) Использовать неправильно.Эти фрагменты кода образуют пример компиляции на Coliru .
источник
Если производный класс - необходимо повторно использовать код и - вы не можете изменить базовый класс, и - он защищает свои методы с помощью членов базы под блокировкой.
тогда вам следует использовать частное наследование, в противном случае существует опасность того, что разблокированные базовые методы будут экспортированы через этот производный класс.
источник
Иногда это может быть альтернативой агрегации , например, если вы хотите агрегацию, но с измененным поведением агрегируемого объекта (переопределение виртуальных функций).
Но вы правы, здесь не так много примеров из реального мира.
источник
Частное наследование должно использоваться, когда отношение не является «является», но новый класс может быть «реализован в терминах существующего класса» или новый класс «работать как» существующий класс.
пример из «Стандарты кодирования C ++ Андрея Александреску, Херб Саттер»: - Учтите, что у двух классов Square и Rectangle есть виртуальные функции для установки их высоты и ширины. Тогда Square не может правильно наследовать от Rectangle, потому что код, который использует изменяемый Rectangle, будет предполагать, что SetWidth не изменяет высоту (независимо от того, явно ли Rectangle документирует это сокращение или нет), тогда как Square :: SetWidth не может сохранить этот контракт и свой собственный инвариант прямоугольности при в то же время. Но Rectangle также не может правильно наследовать от Square, если клиенты Square предполагают, например, что площадь Square равна его ширине в квадрате, или если они полагаются на какое-то другое свойство, которое не выполняется для Rectangles.
Квадрат "является" прямоугольником (математически), но квадрат не является прямоугольником (поведенчески). Следовательно, вместо «is-a» мы предпочитаем говорить «работает как a» (или, если вы предпочитаете, «usable-as-a»), чтобы описание было менее подвержено недоразумениям.
источник
Класс содержит инвариант. Инвариант устанавливается конструктором. Однако во многих ситуациях полезно иметь представление о состоянии представления объекта (которое вы можете передать по сети или сохранить в файл - DTO, если хотите). REST лучше всего использовать с точки зрения AggregateType. Это особенно верно, если вы правы. Рассматривать:
На этом этапе вы можете просто хранить коллекции кешей в контейнерах и просматривать их при создании. Удобно, если есть реальная обработка. Обратите внимание, что кэш является частью QE: операции, определенные в QE, могут означать, что кеш частично повторно используется (например, c не влияет на сумму); тем не менее, когда кеша нет, его стоит поискать.
Частное наследование почти всегда можно смоделировать членом (при необходимости сохраняя ссылку на базу). Просто не всегда стоит так моделировать; иногда наследование является наиболее эффективным представлением.
источник
Если вам нужен
std::ostream
с небольшими изменениями (как в этом вопросе ), вам может потребоватьсяMyStreambuf
производный от него,std::streambuf
и внесите в него изменения.MyOStream
который является производным отstd::ostream
этого, также инициализирует и управляет экземпляромMyStreambuf
и передает указатель на этот экземпляр конструкторуstd::ostream
Первой идеей может быть добавление
MyStream
экземпляра в качестве члена данных кMyOStream
классу:Но базовые классы создаются перед любыми членами данных, поэтому вы передаете указатель на еще не созданный
std::streambuf
экземпляр, дляstd::ostream
которого поведение undefined.Решение предлагается в ответе Бена на вышеупомянутый вопрос , просто наследовать сначала из буфера потока, затем из потока, а затем инициализировать поток с помощью
this
:Однако результирующий класс также можно использовать как
std::streambuf
экземпляр, что обычно нежелательно. Переход на частное наследование решает эту проблему:источник
Тот факт, что в C ++ есть функция, не означает, что она полезна или ее следует использовать.
Я бы сказал, вам вообще не стоит его использовать.
Если вы все равно его используете, ну, вы в основном нарушаете инкапсуляцию и снижаете связность. Вы помещаете данные в один класс и добавляете методы, управляющие данными, в другой.
Как и другие функции C ++, его можно использовать для достижения побочных эффектов, таких как закрытие класса (как упоминалось в ответе dribeas), но это не делает его хорошей функцией.
источник