Почему виртуальное наследование необходимо указывать в середине иерархии алмазов?
У меня есть алмазная иерархия классов:
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 поступают из сторонней библиотеки?
EDIT: Мой ответ состоял в том, чтобы указать классы B и C, что они не должны вызывать конструктор всякий раз, когда его производный объект создается, поскольку он будет вызываться D.
Ответы
Ответ 1
Я не уверен, что именно они решили создать виртуальное наследование таким образом, но я считаю, что причина связана с макетом объекта.
Предположим, что С++ был спроектирован таким образом, чтобы разрешить проблему с алмазом, вы фактически наследовали B и C в D, а не фактически наследовали A в B и C. Теперь, какова будет макет объекта для B и C? Ну, если никто никогда не пытается наследовать от них, то у каждого будет своя копия A и может использовать стандартную оптимизированную компоновку, где B и C имеют A на своей базе. Однако, если кто-то фактически наследует от B или C, тогда макет объекта должен быть другим, потому что двум придется делиться своей копией A.
Проблема заключается в том, что когда компилятор сначала видит B и C, он не может знать, будет ли кто-то наследовать от них. Следовательно, компилятор должен был бы отказаться от более медленной версии наследования, используемой в виртуальном наследовании, а не более оптимизированной версии наследования, которая включена по умолчанию. Это нарушает принцип С++ "не платите за то, для чего вы не используете" (принцип с нулевым уровнем дохода), где вы платите только за языковые функции, которые вы явно используете.
Ответ 2
Почему виртуальное наследование должно выполняться в B и C, хотя двусмысленность в D? Это было бы более интуитивно, если бы оно было в D.
В вашем примере B и C используют virtual
специально, чтобы попросить компилятор обеспечить наличие только одной копии A. Если они этого не сделали, они эффективно говорят: "Мне нужен мой собственный базовый класс, я не собираюсь делиться им с каким-либо другим производным объектом". Это может иметь решающее значение.
Пример того, что вы не хотите делиться виртуальным базовым классом
Если A был каким-то контейнером, B был получен из него и сохранил определенный тип объекта - скажем, "Bat", а C хранит "Cat". Если D ожидает, что B и C независимо предоставят информацию о населении Bats and Cats, они были бы очень удивлены, если бы операция C сделала что-то с Bats или B-операция сделала что-то с кошками. /p >
Пример наличия виртуального базового класса
Говорить D необходимо предоставить доступ к некоторым функциям или элементам данных, которые находятся в A, например "A:: x"... если A наследуется независимо (не виртуально) с помощью B и C, тогда компилятор может "t разрешить 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, который он непосредственно наследует (если ему требуется видимое отношение "A" ). Это может быть практичным, если вызывающий код знает, что он имеет дело с D (даже если с помощью шаблонов), но операции над объектом с помощью указателей на базовые классы не будут знать о том, что управление D пытается выполнить, и все это может быть очень сложно получить право. Но это немного похоже на высказывание "что, если мне нужен вектор, и у меня есть только список", "пила, а не отвертка"... ну, воспользуйтесь ею или получите то, что вам действительно нужно.
EDIT: Мой ответ состоял в том, чтобы указать классы B и C, что они не должны вызывать конструктор всякий раз, когда его производный объект создается, поскольку он будет вызываться D.
Это важный аспект этого, да.
Ответ 3
В дополнение к запросу templatetypedef можно указать, что вы также можете обернуть A в
class AVirt:virtual public A{};
и наследует от него другие классы. Вам не нужно явно указывать другие свойства как виртуальные в этом случае
Ответ 4
Вопрос: Почему виртуальное наследование необходимо выполнять в B и C, хотя двусмысленность в D?
Поскольку методы B и C должны знать, что им, возможно, придется работать над объектами, макет которых сильно отличается от собственных макетов B и C. С единственным наследованием это не проблема, потому что производные классы просто добавляют свои атрибуты после родительского исходного макета.
С множественным наследованием вы не можете этого сделать, потому что вначале нет единого родительского макета. Более того (если вы хотите избежать дублирования) макеты родителей должны пересекаться по атрибутам A. Множественное наследование в С++ скрывает довольно много сложности.
Ответ 5
Поскольку 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;
};