2015-06-01 6 views
7

Я пытаюсь лучше понять концепцию виртуального наследования и каковы его опасности.заказ конструктора виртуального наследования

Я прочитал в другом посте (Why is Default constructor called in virtual inheritance?), что он (= виртуальное наследование) изменяет порядок вызова конструктора (сначала называется «бабушка», а без виртуального наследования это не так).

Так что я попытался следующие, чтобы увидеть, что у меня появилась идея (VS2013):

#define tracefunc printf(__FUNCTION__); printf("\r\n") 
struct A 
{ 
    A(){ tracefunc; } 

}; 

struct B1 : public A 
{ 
    B1(){ tracefunc; }; 
}; 

struct B2 : virtual public A 
{ 
    B2() { tracefunc; }; 
}; 

struct C1 : public B1 
{ 
    C1() { tracefunc; }; 
}; 

struct C2 : virtual public B2 
{ 
    C2() { tracefunc; }; 
}; 

int _tmain(int argc, _TCHAR* argv[]) 
{ 
    A* pa1 = new C1(); 
    A* pa2 = new C2(); 
} 

Выход есть:

A::A 
B1::B1 
C1::C1 
A::A 
B2::B2 
C2::C2 

Который не то, что я ожидал (я ожидал, что порядок 2 класса будут разными).

Что мне не хватает? Может кто-нибудь объяснить или направить меня к источнику, который объясняет это лучше?

Спасибо!

+1

Что вы ожидали? «Бабушка» всегда называется первым. Неважно, какое унаследование вы используете. Если было иначе, как низкоуровневые классы использовали данные родителя в своих конструкторах? – ixSci

+0

Виртуальное наследование только делает разницу в случае множественного наследования. Он предоставляется для распределения некоторой структуры через граф наследования. –

ответ

2

В вашем примере ожидаемый результат. Virtual inheritance вступает в игру в случае, когда у вас есть класс с множественным наследованием, родительские классы которого также наследуются от одного и того же класса/типа (т. Е. «Проблема с алмазом»). В вашем примере ваши классы могут быть настроены практически наследовать (если необходимо в другом месте кода), но они не обязательно «фактически наследуются» на основе вашего примера, поскольку ни один из производных классов (B1/B2/C1/C2) не делает больше, чем наследует напрямую от A.

Для расширения, я подправил свой пример, чтобы объяснить немного больше:

#include <cstdio> 

#define tracefunc printf(__FUNCTION__); printf("\r\n") 
struct A 
{ 
    A() { tracefunc; } 
    virtual void write() { tracefunc; } 
    virtual void read() { tracefunc; } 
}; 

struct B1 : public A 
{ 
    B1() { tracefunc; }; 
    void read(){ tracefunc; } 
}; 

struct C1 : public A 
{ 
    C1() { tracefunc; }; 
    void write(){ tracefunc; } 
}; 

struct B2 : virtual public A 
{ 
    B2() { tracefunc; }; 
    void read(){ tracefunc; } 
}; 

struct C2 : virtual public A 
{ 
    C2() { tracefunc; }; 
    void write(){ tracefunc; } 
}; 

// Z1 inherits from B1 and C1, both of which inherit from A; when a call is made to any 
// of the base function (i.e. A::read or A::write) from the derived class, the call is 
// ambiguous since B1 and C1 both have a 'copy' (i.e. vtable) for the A parent class. 
struct Z1 : public B1, public C1 
{ 
    Z1() { tracefunc; } 
}; 

// Z2 inherits from B2 and C2, both of which inherit from A virtually; note that Z2 doesn't 
// need to inherit virtually from B2 or C2. Since B2 and C2 both virtual inherit from A, when 
// they are constructed, only 1 copy of the base A class is made and the vtable pointer info 
// is "shared" between the 2 base objects (B2 and C2), and the calls are no longer ambiguous 
struct Z2 : public B2, public C2 
{ 
    Z2() { tracefunc; } 
}; 


int _tmain(int argc, _TCHAR* argv[]) 
{ 
    // gets 2 "copies" of the 'A' base since 'B1' and 'C1' don't virtually inherit from 'A' 
    Z1 z1; 
    // gets only 1 "copy" of 'A' base since 'B2' and 'C2' virtualy inherit from 'A' and thus "share" the vtable pointer to the 'A' base 
    Z2 z2; 

    z1.write(); // ambiguous call to write (which one is it .. B1::write() (since B1 inherits from A) or A::write() ?) 
    z1.read(); // ambiguous call to read (which one is it .. C1::read() (since C1 inherits from A) or A::read() ?) 

    z2.write(); // not ambiguous: z2.write() calls C2::write() since it's "virtually mapped" to/from A::write() 
    z2.read(); // not ambiguous: z2.read() calls B2::read() since it's "virtually mapped" to/from A::read() 
    return 0; 
} 

Хотя это может быть «очевидно» для нас, людей, которые называют мы намерены сделать в случае переменной z1, поскольку B1 не имеет метода write, я бы «ожидал» компилятор, чтобы выбрать метод C1::write, но из-за того, как работает отображение объектов памяти, возникает проблема, поскольку базовая копия A в объекте C1 может иметь другая информация (указатели/ссылки/ручки), чем копия базы A в B1 obj ect (поскольку имеется технически 2 копии базы A); поэтому вызов B1::read() { this->write(); } может привести к неожиданному поведению (хотя и не определено).

Ключевое слово virtual в спецификаторе базового класса делает его явным, что другие классы, которые фактически наследуются от одного и того же базового типа, должны получить только 1 копию базового типа.

Обратите внимание, что приведенный выше код не должен компилироваться с ошибками компилятора, объясняя неоднозначные вызовы для объекта z1. Если закомментировать z1.write(); и z1.read(); строки вывода (для меня по крайней мере) имеет следующий вид:

A::A 
B1::B1 
A::A 
C1::C1 
Z1::Z1 
A::A 
B2::B2 
C2::C2 
Z2::Z2 
C2::write 
B2::read 

Обратите внимание на 2 вызовы A CTOR (A::A) до Z1 конструируют, в то время как Z2 имеет только 1 вызов к конструктору A.

Я рекомендую прочитать the following on virtual inheritance, поскольку он более подробно описывает некоторые другие подводные камни, чтобы принять к сведению (например, тот факт, что фактически унаследованные классы должны использовать список инициализации для создания вызовов класса ctor или что вам следует избегать используя приведения типа C при выполнении такого типа наследования).

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

Надеюсь, что это поможет немного разобраться.

0

Вы не сможете увидеть разницу в выходе, так как результат будет тот же в любой из следующих hiearchies класса:

Hiearchy 1

class A {}; 

class B2 : virtual public A {}; 

class C2 : virtual public B2 {}; 

Hiearchy 2

class A {}; 

class B2 : public A {}; 

class C2 : virtual public B2 {}; 

Hiearchy 3

class A {}; 

class B2 : virtual public A {}; 

class C2 : public B2 {}; 

Hiearchy 3

class A {}; 

class B2 : public A {}; 

class C2 : public B2 {}; 

Во всех этих случаях, A::A() будет выполнена первой, а затем B2::B2(), а затем C2::C2().

Разница между ними, когда A::A() вызывается. Вызывается ли он от B2::B2(), или C2::C2()?

Я не уверен, 100% ответ на вопрос Hiearchy 1. Я думаю, B2::B2() должен быть вызван от C2::C2, так как B2 является виртуальным базовым классом C. A::A() следует получить от B2:B2(), так как A является виртуальным базовым классом B2. Но я мог ошибаться в точном порядке.

Иерархия 2, A::A() будет называться от B2::B2(). Начиная с B2 является virtual базовым классом C2, B2::B2() получает звонок от C2::C2(). Поскольку A является нормальным базовым классом B2, A::A() вызывается от B2::B2().

Иерархия 2, A::A() будет называться от C2::C2(). Поскольку A является виртуальным базовым классом, A::A() получает вызов от C2::C2(). B2::B2() вызывается после завершения вызова A::A().

В Иерархия 4, A::A() будет называться от B2::B2(). Я думаю, что этот случай не нуждается в объяснении.

Чтобы прояснить мои сомнения относительно Hiearchy 1, я использовал следующую программу:

#include <iostream> 

class A 
{ 
    public: 
     A(char const *from) { std::cout << "Called from : " << from << std::endl; } 

}; 

class B2 : virtual public A 
{ 
    public: 
     B2() : A("B2::B2()") {} 
}; 

class C2 : virtual public B2 
{ 
    public: 
     C2() : A("C2::C2()") {} 
}; 

int main() 
{ 
    C2 c; 
} 

я получил следующий вывод:

Called from : C2::C2() 

Это подтверждает то, что @TC указанный в своем комментарии, который отличается от того, что я ожидал. A::A() получает вызов от C2::C2, а не от B2::B2.

+2

Виртуальные базы всегда строятся конструктором самого производного класса; их порядок построения - это первый шаг слева направо по пересечению DAG базовых классов. Поэтому в 1 'C2 :: C2()' сначала вызовет 'A :: A()', а затем вызовет 'B2 :: B2()'. –

+0

@ T.C. Спасибо за информацию. Я подтвердил, что вы правы. –

+0

... где «слева направо» относится к порядку появления в коде, а обход - _post-order_. – RJFalconer

1

Выход компилятора является правильным. Фактически, речь идет о цели виртуального наследования. Виртуальное наследование направлено на решение проблемы «Алмаз» в множественном наследовании. Например, В наследует от A, C наследует от A и D наследует от B, C. Схема такова:

A 
    | | 
    B C 
    | | 
    D 

Так, D имеет два экземпляра A от B и C. Если A имеет виртуальные функции , существует проблема.

Например:

struct A 
{ 
    virtual void foo(){__builtin_printf("A");} 
    virtual void bar(){} 
}; 

struct B : A 
{ 
    virtual void foo(){__builtin_printf("B");} 
}; 

struct C : A 
{ 
    virtual void bar(){} 
}; 

struct D : B, C 
{ 

}; 

int main() 
{ 
    D d; 
    d.foo(); // Error 
} 

Если я использую мой XLC компилятор для компиляции и запуска:

сообщение
xlC -+ a.C 

Ошибка такова:

a.C:25:7: error: member 'foo' found in multiple base classes of different types 
    d.foo(); // Error 
    ^
a.C:9:18: note: member found by ambiguous name lookup 
    virtual void foo(){__builtin_printf("B");} 
       ^
a.C:3:18: note: member found by ambiguous name lookup 
    virtual void foo(){__builtin_printf("A");} 
       ^
1 error generated. 
Error while processing a.C. 

Сообщение об ошибке очень ясно, член 'foo' найден в нескольких базовых классах разных типов. Если мы добавим виртуальное наследование, проблема будет решена. Поскольку права строительства обрабатывается D, есть один экземпляр А.

Назад к коду, диаграмма наследования выглядит так:

A  A 
|  | 
B1 B2 
|  | 
C1 C2 

Там нет «Проблемы алмаза», это это только одно наследование. Таким образом, порядок построения также A-> B2-> C2, разности выхода нет.