Подтвердить что ты не робот

Когда виртуальное наследование - хороший дизайн?

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

Привет, я просмотрел вопросы о виртуальном наследовании (class B: public virtual A {...}) в С++, но не нашел ответа на мой вопрос.

Я знаю, что есть некоторые проблемы с виртуальным наследованием, но то, что я хотел бы знать, - это то, в каких случаях виртуальное наследование будет считаться хорошим дизайном.

Я видел, как люди упоминали интерфейсы типа IUnknown или ISerializable, а также что дизайн iostream основан на виртуальном наследовании. Будут ли они хорошими примерами хорошего использования виртуального наследования, заключается в том, что нет лучшей альтернативы или потому, что виртуальное наследование является надлежащим дизайном в этом случае? Спасибо.

EDIT: Чтобы уточнить, я спрашиваю о реальных примерах, пожалуйста, не давайте абстрактных. Я знаю, что такое виртуальное наследование и какой шаблон наследования он требует, что я хочу знать, когда это хороший способ делать что-то, а не просто следствие сложного наследования.

EDIT2: Другими словами, я хочу знать, когда иерархия алмазов (которая является причиной виртуального наследования) - хороший дизайн

4b9b3361

Ответ 1

Если у вас есть иерархия интерфейса и соответствующая иерархия реализации, необходимо сделать базовые классы интерфейса виртуальными базами.

например.

struct IBasicInterface
{
    virtual ~IBasicInterface() {}
    virtual void f() = 0;
};

struct IExtendedInterface : virtual IBasicInterface
{
    virtual ~IExtendedInterface() {}
    virtual void g() = 0;
};

// One possible implementation strategy
struct CBasicImpl : virtual IBasicInterface
{
    virtual ~CBasicImpl() {}
    virtual void f();
};

struct CExtendedImpl : virtual IExtendedInterface, CBasicImpl
{
    virtual ~CExtendedImpl() {}
    virtual void g();
};

Обычно это имеет смысл, если у вас есть несколько интерфейсов, которые расширяют базовый интерфейс и более чем одну стратегию реализации, требуемую в разных ситуациях. Таким образом, у вас есть четкая иерархия интерфейса, и иерархии реализации могут использовать наследование, чтобы избежать дублирования общих реализаций. Если вы используете Visual Studio, вы получаете много предупреждений C4250.

Чтобы предотвратить случайное нарезку, обычно лучше, если классы CBasicImpl и CExtendedImpl не являются реальными, но вместо этого имеют дополнительный уровень наследования, не предоставляя дополнительной функциональности, кроме конструктора.

Ответ 2

Виртуальное наследование - хороший выбор дизайна для случая, когда класс A расширяет другой класс B, но B не имеет виртуальных функций-членов, кроме, возможно, деструктора. Вы можете думать о таких классах, как B, как mixins, где иерархия типов нуждается только в одном базовом классе типа mixin, чтобы извлечь из этого выгоду.

Одним хорошим примером является виртуальное наследование, которое используется с некоторыми из шаблонов iostream в реализации STL в libstdС++. Например, libstdС++ объявляет шаблон basic_istream с помощью:

template<typename _CharT, typename _Traits>
class basic_istream : virtual public basic_ios<_CharT, _Traits>

Он использует виртуальное наследование для расширения basic_ios<_CharT, _Traits>, потому что istreams должен иметь только один входной streambuf, и многие операции с istream всегда должны иметь одинаковую функциональность (в частности, функцию члена rdbuf для получения одного и того же потока streambuf).

Теперь представьте, что вы пишете класс (baz_reader), который расширяет std::istream с помощью функции-члена для чтения в объектах типа baz и другого класса (bat_reader), который расширяет std::istream с помощью элемента функцию для чтения в объектах типа bat. Вы можете иметь класс, который расширяет как baz_reader, так и bat_reader. Если бы виртуальное наследование не использовалось, то базы baz_reader и bat_reader имели бы каждый свой собственный поток streambuf, возможно, не намерение. Вероятно, вы захотите, чтобы базы baz_reader и bat_reader были прочитаны как из одного streambuf. Без виртуального наследования в std::istream для расширения std::basic_ios<char> вы могли бы это сделать, установив члены readbufs из baz_reader и bat_reader оснований на один и тот же объект streambuf, но тогда у вас будет две копии указателя на streambuf, когда этого будет достаточно.

Ответ 3

Grrr.. Виртуальное наследование ДОЛЖНО использоваться для подтипирования абстракции. Нет абсолютно никакого выбора, если вы будете подчиняться принципам дизайна OO. В противном случае другие программисты не получат других подтипов.

Абстрактный пример: у вас есть базовая абстракция A. Вы хотите сделать подтип B. Обратите внимание, что подтип обязательно означает другую абстракцию. Если это не абстрактно, это реализация, а не тип.

Теперь идет еще один программист и хочет сделать подтип C A. Cool.

Наконец, еще один программист приходит и хочет что-то, что является как B, так и C. Это также A, конечно. В этих сценариях виртуальное наследование является обязательным.

Вот пример реального мира: из компилятора, моделирование типов данных:

struct function { ..
struct int_to_float_type : virtual function { ..

struct cloneable : virtual function { .. 

struct cloneable_int_to_float_type : 
  virtual function, 
  virtual int_to_float_type 
  virtual cloneable 
{ ..

struct function_f : cloneable_int_to_float_type { 

Здесь function представляет функции, int_to_float_type представляет собой подтип состоящий из функций от int до float. Cloneable является специальным свойством что функция может быть клонирована. function_f является конкретным (не абстрактным) функция.

Обратите внимание, что если бы я изначально не делал function виртуальную базу int_to_float_type, я не мог бы смешивать Cloneable (и наоборот).

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

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

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

Ответ 4

Виртуальное наследование не является хорошим или плохим - это деталь реализации, как и любая другая, и существует для реализации кода, где происходят одни и те же абстракции. Обычно это правильно, когда код должен быть супер-временем выполнения, например, в COM, где некоторые COM-объекты должны совместно использоваться между процессами, не говоря уже о компиляторах и т.п., Что требует использования IUnknown, где обычные библиотеки С++ будут просто использовать shared_ptr. Как таковой, на мой взгляд, нормальный код на С++ должен зависеть от шаблонов и подобных и не должен требовать виртуального наследования, но он абсолютно необходим в некоторых особых случаях.

Ответ 5

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

В начале 90-х я использовал библиотеку классов, у которой был класс ReferenceCounted, из которого были получены другие классы, чтобы дать ему счетчик ссылок и пару методов управления счетчиком ссылок. Наследование должно было быть виртуальным, иначе, если бы у вас было несколько баз, каждое из которых было получено практически не из ReferenceCounted, у вас получилось бы количество ссылок. Виртуальное наследование гарантировало, что у вас будет один счетчик ссылок для ваших объектов.

Неинтрузивный подсчет ссылок с shared_ptr и другими кажется более популярными в наши дни, но интрузивный подсчет ссылок по-прежнему полезен, когда класс передает this другим методам. В этом случае теряется количество внешних ссылок. Мне также нравится, что интрузивный подсчет ссылок говорит о классе, как управляется жизненный цикл объектов этого класса.

[Я думаю, что я защищаю интрузивный подсчет ссылок, потому что я вижу это так редко в эти дни, но мне это нравится.]

Ответ 6

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

Ответ 8

Виртуальное наследование необходимо, если вы принудительно, чтобы использовать множественное наследование. Есть некоторые проблемы, которые не могут быть легко/легко решены, избегая множественного наследования. В тех случаях (которые являются редкими) вам нужно будет посмотреть на виртуальное наследование. В 95% случаев вы можете (и должны) избегать множественного наследования, чтобы спасти себя (и тех, кто смотрит на ваш код после вас) много головных болей.

Как примечание, COM не заставляет вас использовать множественное наследование. Возможно (и довольно часто) создать COM-объект, полученный из IUnknown (прямо или косвенно), который имеет дерево линейного наследования.