Почему виртуальное наследование нужно указывать в середине алмазной иерархии? - PullRequest
11 голосов
/ 02 марта 2011

У меня есть алмазная иерархия классов:

    A
  /   \
 B     C
  \   /
    D

Чтобы избежать двух копий A в D, нам нужно использовать виртуальное наследование в B и C.

class A                     {  }; 
class B: virtual public A {};
class C: virtual public A   { }; 
class D: public B, public C { }; 

Вопрос: Почему виртуальное наследование необходимо выполнять в точках B и C, хотя неоднозначность находится в точке D? Это было бы более интуитивно понятно, если бы оно было на D.

Почему эта функция разработана комитетом по стандартам? Что мы можем сделать, если классы B и C приходят из сторонней библиотеки?

РЕДАКТИРОВАТЬ: Мой ответ состоял в том, чтобы указать классам B и C, что они не должны вызывать конструктор A всякий раз, когда создается его производный объект, как это будет вызываться D.

Ответы [ 5 ]

8 голосов
/ 02 марта 2011

Почему виртуальное наследование должно выполняться в B и C, даже если неопределенность в D? Это было бы более интуитивно понятно, если бы оно было на D.

В вашем примере B и C используют virtual специально, чтобы попросить компилятор убедиться, что задействована только одна копия A. Если они этого не сделали, они фактически говорят: «Мне нужен мой собственный базовый класс, я не собираюсь делиться им с любым другим производным объектом». Это может иметь решающее значение.

Пример нежелания делиться виртуальным базовым классом

Если A был чем-то вроде контейнера, B был получен из него и хранил некоторый конкретный тип объекта - скажем, "Bat", в то время как C хранит "Cat". Если D ожидает, что B и C будут независимо предоставлять информацию о популяции летучих мышей и кошек, они будут очень удивлены, если операция C сделала что-то с / с летучими мышами, или операция B сделала что-то с / с кошками.

Пример использования виртуального базового класса

Скажем, D должен предоставить доступ к некоторым функциям или членам данных, которые находятся в A, скажем, "A :: x" ... если A наследуется независимо (не виртуально) B и C, то компилятор может Разрешить D :: x в B :: x или C :: x без необходимости явного устранения неоднозначности программисту. Это означает, что D не может быть использован в качестве A, несмотря на наличие не одной, а двух отношений "is-a", подразумеваемых цепочкой деривации (т. Е. Если B "представляет собой" A, а D "представляет собой" B, то пользователь может ожидать / нужно использовать D, как если бы D "было" A).

Почему эта функция разработана комитетом по стандартам?

virtual наследование существует, потому что оно иногда полезно. Он определяется B и C, а не D, потому что это навязчивая концепция с точки зрения дизайна B и C, а также имеет значение для инкапсуляции, размещения памяти, создания и уничтожения и распределения функций B и C.

Что мы можем сделать, если классы B и C поступают из сторонней библиотеки?

Если D необходимо наследовать от обоих и предоставлять доступ к A, но B и C не предназначены для использования виртуального наследования и не могут быть изменены, то D должен взять на себя ответственность за пересылку любых запросов, соответствующих API A, на либо B и / или C, и / или, необязательно, другой A, от которого он напрямую наследует (если ему нужны видимые отношения "is A"). Это может быть практичным, если вызывающий код знает, что он имеет дело с D (даже если с помощью шаблонов), но операции над объектом через указатели на базовые классы не будут знать об управлении, которое D пытается выполнить, и все это может быть очень сложно получить права. Но это все равно, что сказать «что, если мне нужен вектор, а у меня есть только список», «пила, а не отвертка» ... ну, максимально используйте это или получите то, что вам действительно нужно.

РЕДАКТИРОВАТЬ: Мой ответ состоял в том, чтобы указать классам B и C, что они не должны вызывать конструктор A всякий раз, когда создается его производный объект, так как он будет вызываться D.

Это важный аспект этого, да.

8 голосов
/ 02 марта 2011

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

Предположим, что C ++ был разработан таким образом, чтобы решитьВ случае с бриллиантом вы бы фактически унаследовали B и C в D, а не фактически унаследовали A в B и C. Теперь, каким будет расположение объектов для B и C?Ну, если бы никто никогда не пытался фактически наследовать от них, то у каждого из них была бы своя собственная копия A, и они могли бы использовать стандартную оптимизированную компоновку, где у B и C каждый имел A в своей основе.Однако, если кто-то действительно фактически наследует от B или C, то расположение объектов должно быть другим, потому что им придется делиться своей копией A.

Проблема с этимв том, что когда компилятор впервые видит B и C, он не может знать, будет ли кто-то наследовать от них.Следовательно, компилятору придется использовать более медленную версию наследования, используемую в виртуальном наследовании, а не более оптимизированную версию наследования, которая включена по умолчанию.Это нарушает принцип C ++ «не платите за то, что вы не используете» ( принцип нулевых накладных расходов ), когда вы платите только за языковые функции, которые вы явно используете.

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

В дополнение к ответу templatetypedef можно указать, что вы также можете заключить A в

class AVirt:virtual public A{}; 

и наследовать от него другие классы. В этом случае вам не нужно явно отмечать другие объекты как виртуальные

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

Вопрос: Почему виртуальное наследование необходимо выполнять в точках B и C, хотя неоднозначность находится в точке D?

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

При множественном наследовании вы не можете этого сделать, потому что в первую очередь нет макета одного родителя. Более того (если вы хотите избежать дублирования А), макеты родителей должны накладываться на атрибуты А. Множественное наследование в C ++ скрывает довольно большую сложность.

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

Поскольку A является классом с множественным наследованием, именно классы, производные от него, должны делать это виртуальным.

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

Обойти что-то вроде этого:

class B : public A; // not your class, cannot change
class C : public A; // not your class, cannot change

class D : public B; // your class, implement the functions of B
class D2 : public C; // your class implement the functions of C

class D
{
   D2 d2;
};
...