6 февр. 2009 г.

Нюансы множественного наследования в C++

Недавно открыл для себя виртуальное наследование в C++.

Такой способ наследования тесно связан с другой возможностью C++ - множественным наследованием. Я бы даже сказал, он вполне логично дополняет один из способов применения множественного наследования.

Рассмотрим такой пример:

class A {
public:
int var_a;
virtual void foo() = 0;
};

class B: public A {
public:
int var_b;
void bar()
{
foo();
}
};

class C: public A {
public:
int var_c;
void foo()
{
cout << "This is C::foo()" << endl;
}
};

class D: public B, public C {
int var_d;
};
Все дальнейшие манипуляции с классами будем проводить в main().
Например, объявим экземпляр класса D и попробуем присвоить что-нибудь переменной var_a:
D d;
d.var_a = 2;
Компилятор не пропустит такое выражение и вот почему. Дело в том, что при использовании обычного невиртуального наследования экземпляр объекта D имеет следующую логическую структуру:

Как видно из схемы, мы имеем две переменных var_a, одна их которых принадлежит классу A, который является базовым классом для B, а другая принадлежит классу A, который является базовым классом для C.
Для обхода этой ситуации можно конечно явно указать, какую переменную мы имеем ввиду:
d.B::A::var_a = 2;
или:
d.C::A::var_a = 2;
Но это, согласитесь, не очень красиво, да и не всегда логически необходимо иметь два экземпляра базового класса.
Вот именно в этом случае к нам приходит на помощь виртуальное наследование.
Есть и еще один нюанс в нашем примере: взгляните на функцию B::bar(). В ней происходит вызов виртуальной функции A::foo(), которая для этой ветви иерархии неопределена - ни в классе A, ни в классе B, ни в классе D. К классу C функция B::bar() доступа не имеет, так как C по сути наследует от другого экземпляра A.

Что же будет, если применить виртуальное наследование? Слегка изменим наш пример:
class A {
public:
int var_a;

virtual void foo() = 0;
};


class B: public virtual A {
public:
int var_b;

void bar()
{
foo();
}
};


class C: public virtual A {
public:
int var_c;
void foo()

{
cout << "This is C::foo()" << endl;
}
};

class D: public B, public C {
int var_d;
};

Теперь экземпляр класса D будет иметь другую логическую структуру:

Как видно из рисунка теперь для классов B и C создается единственный общий экземпляр класса A.
Таким образом код
D d;
d.var_a = 2;

становится допустимым.

При применении виртуального наследования становится возможном и еще один интересный финт ушами - использование виртуальной функции, фактически определенной не в родительском и не в дочернем классах, а в классе-брате. В нашем примере - это виртуальная функция A::foo(), которая определена в классе C - C::foo(). Посмотрите, она используется из функции B::bar(), который является братом класса C.
Естественно, чтобы это работало, мы должны создать экземпляр такого объекта, который бы наследовал от обоих классов B и C, в нашем случае - это класс D. Таким образом, становится допустимым код:
D d;
B& b = d; //создали ссылку типа B на объект типа D.
b.bar(); //вызываем функцию bar(), которая в свою очередь вызывает функцию foo(), которая фактически определена в классе C.

Программиста, который получил в свой метод ссылку типа B, вообще может ввести в заблуждение работоспособность подобного кода, ведь он не знает о том, что на самом деле эта ссылка указывает на объект типа D (он вовсе не видит объявления класса D), который наследует и от C тоже. С его точки зрения это будет казаться мистикой - ведь функция bar() вызывает виртуальную функцию foo(), которая с его точки зрения НИГДЕ не определена!

Ну и напоследок еще одна задачка, связанная с множественным наследованием:

class B
{
public:
int var_b;
};

class C
{
public:
int var_c;
};

class D: public B, public C
{
public:
int var_d;
};

using namespace std;

int main()
{
D* d = new D;

B* b = d;
C* c = d;

void* v_d = d;
void* v_b = b;
void* v_c = c;

cout << (b == d) << endl;
cout << (c == d) << endl;

cout << endl;

cout << (v_b == v_d) << endl;
cout << (v_c == v_d) << endl;

return 0;
}
Программа выведет:
1
1
1
0

Внимание вопрос: почему в последнем случае получается ноль (false)? :)

3 комментария: