В отличие от защищенного наследования, частное наследование C ++ нашло свое отражение в основных разработках C ++. Однако я до сих пор не нашел ему хорошего применения.
Когда вы, ребята, этим пользуетесь?
В отличие от защищенного наследования, частное наследование 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:
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...
};
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 ++» за эту формулировку). Другими словами, внешний интерфейс наследующего класса не имеет (видимого) отношения к унаследованному классу, но использует его внутри для реализации своих функций.
Одно из полезных применений частного наследования - это когда у вас есть класс, реализующий интерфейс, который затем регистрируется с другим объектом. Вы делаете этот интерфейс закрытым, так что сам класс должен регистрироваться, и только конкретный объект, с которым он зарегистрирован, может использовать эти функции.
Например:
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, в то время как другие внешние классы не могут. Это отличный шаблон для обработки определенных обратных вызовов, которые определены как интерфейсы.
Я думаю, что критический раздел из C ++ FAQ Lite :
Законное долгосрочное использование частного наследования - это когда вы хотите создать класс Fred, который использует код в классе Wilma, а код из класса Wilma должен вызывать функции-члены из вашего нового класса, Fred. В этом случае Фред вызывает невиртуалы в Вилме, а Вильма вызывает (обычно чистые виртуалы) сама по себе, которые заменяются Фредом. Сделать это с композицией будет намного сложнее.
В случае сомнений вам следует предпочесть композицию частному наследованию.
Я считаю это полезным для интерфейсов (а именно абстрактных классов), которые я наследую, когда я не хочу, чтобы другой код касался интерфейса (только наследующий класс).
[отредактировано в примере]
Возьмите пример, приведенный выше. Говоря это
[...] класс Wilma должен вызывать функции-члены из вашего нового класса, Фред.
означает, что Вильма требует, чтобы Фред мог вызывать определенные функции-члены, или, скорее, он говорит, что Вильма - это интерфейс . Следовательно, как указано в примере
частное наследование - это не зло; просто поддерживать его дороже, так как увеличивается вероятность того, что кто-то изменит что-то, что нарушит ваш код.
комментирует желаемый эффект от программистов, которые должны соответствовать нашим требованиям к интерфейсу или нарушать код. И, поскольку fredCallsWilma () защищен, только друзья и производные классы могут касаться его, то есть унаследованного интерфейса (абстрактного класса), который может касаться только наследующий класс (и друзья).
[отредактировано в другом примере]
На этой странице кратко обсуждаются частные интерфейсы (еще под другим углом).
Иногда мне кажется полезным использовать частное наследование, когда я хочу предоставить меньший интерфейс (например, коллекцию) в интерфейсе другого, где реализация коллекции требует доступа к состоянию выставляющего класса аналогично внутренним классам в Ява.
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
есть ли в этом примере? Мне это интересно, но это кричит мне в лицо.
Я нашел хорошее приложение для частного наследования, хотя его использование ограничено.
Предположим, вам предоставлен следующий 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:
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 ++, так почему бы не использовать его все возможности?
Вышеупомянутые проблемы в основном связаны с ручным управлением ресурсами. На ум приходит решение наследовать от 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 .
Если производный класс - необходимо повторно использовать код и - вы не можете изменить базовый класс, и - он защищает свои методы с помощью членов базы под блокировкой.
тогда вам следует использовать частное наследование, в противном случае существует опасность того, что разблокированные базовые методы будут экспортированы через этот производный класс.
Частное наследование должно использоваться, когда отношение не является «является», но новый класс может быть «реализован в терминах существующего класса» или новый класс «работать как» существующий класс.
пример из «Стандарты кодирования C ++ Андрея Александреску, Херб Саттер»: - Учтите, что у двух классов Square и Rectangle есть виртуальные функции для установки их высоты и ширины. Тогда Square не может правильно наследовать от Rectangle, потому что код, который использует изменяемый Rectangle, будет предполагать, что SetWidth не изменяет высоту (независимо от того, явно ли Rectangle документирует это сокращение или нет), тогда как Square :: SetWidth не может сохранить этот контракт и свой собственный инвариант прямоугольности при в то же время. Но Rectangle также не может правильно наследовать от Square, если клиенты Square предполагают, например, что площадь Square равна его ширине в квадрате, или если они полагаются на какое-то другое свойство, которое не выполняется для Rectangles.
Квадрат "является" прямоугольником (математически), но квадрат не является прямоугольником (поведенчески). Следовательно, вместо «is-a» мы предпочитаем говорить «работает как a» (или, если вы предпочитаете, «usable-as-a»), чтобы описание было менее подвержено недоразумениям.
Класс содержит инвариант. Инвариант устанавливается конструктором. Однако во многих ситуациях полезно иметь представление о состоянии представления объекта (которое вы можете передать по сети или сохранить в файл - 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 не влияет на сумму); тем не менее, когда кеша нет, его стоит поискать.
Частное наследование почти всегда можно смоделировать членом (при необходимости сохраняя ссылку на базу). Просто не всегда стоит так моделировать; иногда наследование является наиболее эффективным представлением.
Если вам нужен std::ostream
с небольшими изменениями (как в этом вопросе ), вам может потребоваться
MyStreambuf
производный от него, std::streambuf
и внесите в него изменения.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
которого поведение undefined.
Решение предлагается в ответе Бена на вышеупомянутый вопрос , просто наследовать сначала из буфера потока, затем из потока, а затем инициализировать поток с помощью 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 }
{}
};
Тот факт, что в C ++ есть функция, не означает, что она полезна или ее следует использовать.
Я бы сказал, вам вообще не стоит его использовать.
Если вы все равно его используете, ну, вы в основном нарушаете инкапсуляцию и снижаете связность. Вы помещаете данные в один класс и добавляете методы, управляющие данными, в другой.
Как и другие функции C ++, его можно использовать для достижения побочных эффектов, таких как закрытие класса (как упоминалось в ответе dribeas), но это не делает его хорошей функцией.