На відміну від захищеного успадкування, C ++ приватне успадкування знайшло свій шлях у розвиток основного C ++. Однак я все ще не знайшов для цього гарного використання.
Коли ви, хлопці, користуєтесь ним?
На відміну від захищеного успадкування, C ++ приватне успадкування знайшло свій шлях у розвиток основного C ++. Однак я все ще не знайшов для цього гарного використання.
Коли ви, хлопці, користуєтесь ним?
Відповіді:
Примітка після прийняття відповіді: це НЕ повна відповідь. Прочитайте інші відповіді як тут (концептуально), так і тут (як теоретичні, так і практичні), якщо вас цікавить питання. Це просто химерна хитрість, яку можна досягти приватним успадкуванням. Хоча це фантазія, це не відповідь на питання.
Окрім основного використання лише приватного успадкування, показаного у 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 // ...
Я ним користуюся постійно. Кілька прикладів з моєї голови:
Типовий приклад - приватне отримання контейнера 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
перевантажуйте!)
Канонічне використання приватного успадкування - це "реалізовані з точки зору" відносини (завдяки "ефективному С ++" Скотта Майєрса для цієї редакції). Іншими словами, зовнішній інтерфейс класу успадкування не має (видимого) відношення до спадкового класу, але він використовує його внутрішньо для реалізації його функціональності.
Одним корисним використанням приватного успадкування є те, коли у вас є клас, який реалізує інтерфейс, який потім реєструється з яким-небудь іншим об'єктом. Ви робите цей інтерфейс приватним, так що сам клас повинен реєструватися, і лише ті конкретні об'єкти, з якими він зареєстрований, можуть використовувати ці функції.
Наприклад:
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 є:
Законним, довготерміновим використанням для приватного успадкування є те, коли ви хочете створити клас Фред, який використовує код у класі Вільми, а код класу Вільма повинен викликати функції члена з вашого нового класу, Фред. У цьому випадку Фред називає невіртуальними у Вільмі, а Вільма називає (як правило, чистими віртуалами) сам по собі, які переосмислюються Фредом. З композицією це було б набагато складніше.
Якщо ви сумніваєтесь, вам слід віддати перевагу композиції над приватною спадщиною.
Мені здається корисним для інтерфейсів (а саме абстрактних класів), які я успадковую там, де не хочу, щоб інший код торкався інтерфейсу (лише клас успадкування).
[відредаговано в прикладі]
Візьміть приклад, пов'язаний вище. Кажучи це
[...] Клас Вільма повинен викликати функції члена з вашого нового класу, Фред.
означає, що Вільма вимагає, щоб Фред мав змогу викликати певні функції учасника, а точніше, це говорить, що Вільма є інтерфейсом . Отже, як зазначено в прикладі
приватне успадкування не є злим; просто дорожче підтримувати, оскільки це збільшує ймовірність того, що хтось змінить щось, що порушить ваш код.
коментарі щодо бажаного ефекту від програмістів, які потребують задоволення наших вимог до інтерфейсу, або порушення коду. І, оскільки fredCallsWilma () захищений, лише друзі, а похідні класи можуть торкатися його, тобто спадковий інтерфейс (абстрактний клас), до якого може торкатися лише клас спадкування (та друзі).
[відредаговано в іншому прикладі]
На цій сторінці коротко обговорюються приватні інтерфейси (з іншого боку).
Іноді мені здається корисним використовувати приватне успадкування, коли я хочу відкрити менший інтерфейс (наприклад, колекцію) в інтерфейсі іншого, коли реалізація колекції вимагає доступу до стану класу, що відкриває, аналогічно внутрішнім класам у 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
цьому прикладі немає необхідності в прямому оголошенні ? Мені це здається цікавим, але це кричить хакіт в моє обличчя.
Я знайшов гарне застосування для приватного успадкування, хоча воно має обмежене використання.
Припустимо, вам надано наступний 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:
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 ++ стандартів кодування Андрія Олександреску, Herb Sutter": - Врахуйте, що два класи Square і Rectangle мають віртуальні функції для встановлення їх висоти та ширини. Тоді Square не може правильно успадкувати прямокутник, тому що код, який використовує модифікований Прямокутник, передбачає, що SetWidth не змінює висоту (чи прямокутний прямокутник документує контракт, чи ні), тоді як Square :: SetWidth не може зберегти цей контракт та власну інваріантність квадратів у одночасно. Але Прямокутник також не може правильно успадкувати Квадрат, якщо клієнти Площі припускають, наприклад, що площа Площі має ширину у квадраті, або якщо вони покладаються на якусь іншу властивість, яка не відповідає правилам Прямокутників.
Квадрат прямокутника "є-а" (математично), але квадрат - це не прямокутник (поведінковий). Отже, замість "є-а", ми вважаємо за краще сказати "працює-як-а" (або, якщо ви віддаєте перевагу, "корисний-як-а"), щоб зробити опис менш схильним до непорозумінь.
Клас має інваріант. Інваріант встановлюється конструктором. Однак у багатьох ситуаціях корисно мати перегляд стану представлення об'єкта (який ви можете передати через мережу або зберегти у файл - 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 не впливає на суму); все ж, коли немає кешу, варто переглянути його.
Приватне успадкування може майже завжди моделюватися членом (зберігаючи посилання на базу, якщо потрібно). Просто не завжди варто так моделювати; іноді успадкування є найбільш ефективним представництвом.
Якщо вам потрібні 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
який є невизначена поведінка.
Рішення пропонується у відповіді Бена на вищезазначене питання , просто успадкуйте спочатку буфер потоку, потім від потоку, а потім ініціалізуйте потік за допомогою 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 ++, його можна використовувати для досягнення побічних ефектів, таких як герметизація класу (як згадується у відповіді Дрибея), але це не робить його гарною особливістю.