Вы когда-нибудь НЕ хотите использовать виртуальное наследование взаимного базового класса, если вы используете множественное наследование is-a? - PullRequest
23 голосов
/ 22 марта 2011

Если у вас есть наследственные отношения, реализованные с помощью публичного наследования, и у вас есть алмаз наследования, у вас будет что-то вроде:

  • поток класса
  • входной поток и классы выходного потока, полученные из потока
  • класс потока ввода / вывода, полученный из обоих

В этом случае, как используется в стандартной библиотеке (?), В той степени, в которой iostream является istream и is-ostream, istream isa-поток, а ostream is-поток, и, кроме того, они если тот же поток , то любые функции в потоке, которые имеет смысл применять к iostream, должны иметь дело с одной и той же базовой структурой.

В C ++ для совместного использования копий потока в istream и ostream они должны быть фактически унаследованы.

Однако, если вы предпочитаете, вы можете не наследовать виртуально, и каждый раз, когда вы ссылаетесь на члена базового класса, укажите, какую из двух копий (одну в istream или одну в ostream) вы хотите (либо путем приведения, либо с помощью scope :: blah).

У меня такой вопрос, [править: есть ли другой случай, когда] кроме «Это действительно не является отношениями, я использовал капризное публичное наследование как синтаксическое удобство, когда оно не было концептуально обоснованным» или «Мне никогда не нужно полиморфно ссылаться на базовый класс из самого производного класса, так что невероятно небольшие накладные расходы не стоят», есть какая-то причина, по которой было бы концептуально допустимо наследовать не виртуально и иметь две копии базы класс, один для каждой сестры промежуточного класса?

Ответы [ 7 ]

3 голосов
/ 22 марта 2011

Давайте рассмотрим простой пример.

   B           B1   B2
   |             \ /
   D              D

Слева мы находим класс, производный от одного баса.Очевидно, что в ОО-дизайне разумно иметь отношения is-a, уважающие принцип замены Лискова.

Справа оба B1 и B2 являются независимыми основаниями D, и D может бытьдоступ полиморфно с использованием B1* или B2* (или ссылок).Надеемся, что мы можем принять, что это также допустимый ОО-проект (несмотря на то, что Sun считает его слишком напряженным для программистов на Java ;-P).Затем рассмотрите возможность использования некоторой базовой функциональности как из B1, так и B2 и сделайте ее многоразовой: скажем, они оба работают с произвольным списком чисел, которым они могут разумно предоставить прямой / public доступ:

   Container<int>   Container<int>
              \     /
              B1   B2
                \ /
                 D

Если это еще не щелчок, возможно:

   Container<int>   Container<int>
              \     /
            Ages  Heights
                \ /
             Population

Разумно сказать, что Ages is-a Container<int>, хотя это может добавить некоторые удобные функции, такие как average,min, max, num_teenagers.То же самое для Heights, возможно, с другим набором вспомогательных функций.Очевидно, что Population может быть разумно заменено на Ages или Heights коллекцию (например, size_t num_adults(Ages&); if (num_adults(my_population)) ...).

Дело в том, что каждый поддерживающий контейнер не должен иметь 1: 1связь с другими производными классами, такими как Population;скорее, он должен быть точно 1: 1 с его непосредственно полученным классом .

Опять же, использование состава или наследования является решением дизайна интерфейса, но это не обязательно неверно для предоставленияконтейнеры публично таким образом.(Если есть проблема с поддержкой Population инвариантов, таких как Ages::empty() == Heights::empty(), функции преобразования данных Container<int> могут быть сделаны protected, в то время как const функции-члены были public.)

Как выобратите внимание, что Population не имеет однозначного отношения is с Container<int>, и для кода может потребоваться явное устранение неоднозначности.Это уместно.Конечно, для Population также возможно и разумно получить из Container<int>, если он хранит какой-то другой набор чисел, но это будет независимо от косвенно унаследованных контейнеров.

Мой вопрос, кроме того, что «это действительно не отношения, я использовал непонятное публичное наследование как синтаксическое удобство, когда оно не было концептуально обоснованным»

Я не вижу проблем с is-отношения или концептуальная обоснованность вышесказанного, если вы действительно объясните, пожалуйста ...

"Мне никогда не нужно полиморфно ссылаться на базовый класс из самого производного класса, так что невероятно небольшие издержкиэто того не стоит "

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

2 голосов
/ 22 марта 2011

Я бы сказал, что это может привести к хаосу.

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

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

Если в вашей иерархии несколько раз один и тот же базовый класс, то вы можете получить два Base*, которые относятся к одному и тому же объекту ... и при этом различаются (указывают на разностный адрес).

Конечно, вы можете использовать свои трюки и использовать dynamic_cast<void*>(p), чтобы получить «реальный» физический адрес всего объекта ... но все же.

1 голос
/ 22 марта 2011

Проблема заключается в идентичности; в случае с iostream иерархия, общие базы даже имеют состояние, которое устанавливается манипуляторы. Если бы было более одного экземпляра basic_ios, у вас будет две копии состояния (форматирование флаги, состояние ошибки, даже streambuf), который будет катастрофа.

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

0 голосов
/ 22 марта 2011
struct managed
{
    report(std::string what);

private:
    manager_type manager;

protected:
    managed(manager_type);
};

struct human : managed
{
    human() : managed(god) {}
};

struct robot : managed
{
    robot(manager_type owner) : managed(owner) {}
};

struct employee : managed
{
    employee(manager_type boss) : managed(boss) {}
};

struct human_employee : human, employee
{
    human_employee(manager_type boss) : employee(boss) {}
};

struct robot_employee : robot, employee
{
    robot_employee(manager_type owner) : robot(owner), employee(owner) {}
};

Теперь рассмотрим:

void do_some_duty(const employee& e)
{
    e.do_some_tasks();
    e.report("done");
}

void face_disaster(const human& h)
{
    h.report("Oh my!");
}

void commit_suicide(const managed& m)
{
    m.report("I want to suicide");
}

и как человек-сотрудник, тот, о котором вы сообщаете, отличается, если вы работаете не:

human_employee h;
if (h.at_work()) commit_suicide(static_cast<const employee&>(h));
else commit_suicide(static_cast<const human&>(h));

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

0 голосов
/ 22 марта 2011

Теоретически, я могу представить причину для наследования "сестер" B и C от общего A не виртуально в частном порядке , а затем публично наследовать некоторый "составной класс" D от A и B.

Разумеется, это не отношение is_a между большинством производных (D) и большинством базовых классов (A), поскольку частное наследование не демонстрирует отношения is_a к пользователям и другим потомкам.

Однако, если Bи C родственные классы не предназначены специально для этого случая (т.е. для наследования алмазов), они могут быть публично унаследованы от A.

Эффект почти такой же, как и для частного наследования: мы не можем получить доступ к членам Aиз D ( [отредактировано : по крайней мере без явного приведения к сестрам; ] любая попытка будет неоднозначной).Таким образом, можно рассматривать публичное невиртуальное наследование как замену частному невиртуальному наследованию.

Но я бы никогда не стал делать такие трюки.Само наследование является полезной метафорой, но не основой вселенной, и использование ее тонких функций приводит к ненужному сложному замыслу.

IMO, если замена простого (невиртуального) наследования на виртуальное (концептуально) недопустима, почти наверняка есть недостаток дизайна.

Так что я бы ответил «нет» (несмотря на мою «говорящую по-настоящему ...» конструкцию :)

0 голосов
/ 22 марта 2011

Могут быть случаи, когда это полезно.Скажите, класс touchscreen происходит от display_device и input_device?Теперь предположим, что каждый базовый общий базовый класс имеет поле estimated_power_consumption, не будет ли полезно избежать виртуального наследования?

0 голосов
/ 22 марта 2011

Пример

class Category
{   
public:
    virtual std::string CatName(); 
};

class OxygenBreath : public Category
{
public:
    virtual void Breath() = 0;
    std::string CatName(){ return "OxygenBreath";}
};

class LandWalk : public Category  
{
public:
    virtual void Walk() = 0;
    std::string CatName(){ return "LandWalk";}
};

class Human : public OxygenBreath, public LandWalk
{};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...