Допустимо ли использовать виртуальное наследование для предотвращения случайного создания бриллианта? - PullRequest
1 голос
/ 10 сентября 2009

Это упрощение некоторого реального кода, и настоящая ошибка, которую я допустил, когда я не осознавал, что кто-то другой уже реализовал Foo и извлек из него.

#include <iostream>

struct Base {
   virtual ~Base() { }
   virtual void print() = 0;
};

struct OtherBase {
   virtual ~OtherBase() { }
};

struct Foo : public Base { // better to use virtual inheritance?
   virtual void print() { std::cout << "Foo" << std::endl; };
};

struct Bar : public Base { // better to use virtual inheritance?
   virtual void print() { std::cout << "Bar" << std::endl; };
};

// The design is only supposed to implement Base once, but I
// accidentally created a diamond when I inherited from Bar also.
class Derived
   : public OtherBase
   , public Foo
   , public Bar // oops.
{
};

int main() {
   Derived d;
   OtherBase *pO = &d;

   // cross-casting
   if (Base *pBase = dynamic_cast<Base *>(pO))
      pBase->print();
   else
      std::cout << "fail" << std::endl;
}

РЕДАКТИРОВАТЬ : чтобы избавить вас от необходимости запускать этот код ...

  • При запуске как есть, выводится сообщение «ошибка» (нежелательно, трудно отладить).
  • Если вы удалите строку, помеченную «oops», будет напечатано «Foo» (желаемое поведение).
  • Если вы оставите «упс» и сделаете два наследования виртуальными, он не скомпилируется (но, по крайней мере, вы знаете, что исправить).
  • Если вы удалите «упс» и сделаете их виртуальными, он скомпилируется и выведет «Foo» (желаемое поведение).

При виртуальном наследовании результаты либо хорошие, либо ошибка компилятора. Без виртуального наследования результаты либо хорошие, либо необъяснимые, трудно отлаживаемые ошибки времени выполнения.


Когда я реализовал Bar, который в основном дублировал то, что уже делал Foo, это привело к сбою динамического приведения, что означало плохие вещи в реальном коде.

Сначала я был удивлен, что не было ошибки компилятора. Затем я понял, что виртуального наследования не существует, что могло бы вызвать ошибку «нет уникального окончательного переопределения» в GCC. Я намеренно решил не использовать виртуальное наследование, поскольку в этом дизайне не должно быть никаких бриллиантов.

Но если бы я использовал виртуальное наследование при наследовании от Base, код работал бы так же хорошо (без моих опций), и меня бы предупредили о бриллианте во время компиляции и необходимости отслеживать ошибку при запуске время.

Итак, вопрос в том, считаете ли вы допустимым использование виртуального наследования для предотвращения повторения подобной ошибки в будущем? Здесь нет веских технических причин (которые я вижу) для использования виртуального наследования, поскольку в дизайне никогда не должно быть ромба. Это было бы только для обеспечения соблюдения этого проектного ограничения.

Ответы [ 3 ]

2 голосов
/ 10 сентября 2009

Алмаза здесь нет!
То, что вы создали, было множественным наследованием. Каждый базовый класс имеет свою собственную копию Base.

pO имеет тип OtherBase *.
Нет способа преобразовать объект OtherBase * в тип Base *.
Таким образом, динамическое приведение вернет нулевой указатель.

Проблема в том, что динамическое приведение во время выполнения имеет указатель на объект Derived. Но добраться отсюда до базы - это амбициозная операция, которая не может быть выполнена с NULL. Нет ошибки компилятора, так как dynamic_cast является операцией во время выполнения. (Вы можете попытаться привести от чего угодно к чему угодно, и в результате при неудаче получится значение NULL (или выкинуть, если используете ссылки)).

Два варианта:

  • Вы можете заставить dynamic_cast генерировать исключения, если вы применяете ссылки.
  • Или вы можете использовать приведение, проверенное во время компиляции static_cast <>

Проверьте это с помощью:

struct Base
{
    Base(int x): val(x) {}
    int val;
...

struct Foo : public Base
{
    Foo(): Base(1)  {}
.... 

struct Bar : public Base
{
    Bar(): Base(2)  {}
....


// In main:
    std::cout << "Foo" << dynamic_cast<Foo&>(d).val << "\n"
              << "Bar" << dynamic_cast<Bar&>(d).val << "\n";


> ./a.exe  
fail
Foo1
Bar2

Проверка времени компиляции:

std::cout << static_cast<Base*>(pO) << "\n"; // Should fail to compile.
std::cout << static_cast<Base*>(&d) << "\n"; // Will only fail if ambigious.
                                             // So Fails if Foo and Bar derived from Base
                                             // But works if only one is derived.
2 голосов
/ 10 сентября 2009

Не очень хорошая идея.

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

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

1 голос
/ 11 сентября 2009

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

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

...