Подтвердить что ты не робот

Где "виртуальное" ключевое слово необходимо в сложной иерархии множественного наследования?

Я понимаю основы виртуального наследования С++. Однако я смущен тем, где именно мне нужно использовать ключевое слово virtual со сложной иерархией классов. Например, предположим, что у меня есть следующие классы:

            A
           / \
          B   C
         / \ / \
        D   E   F
         \ / \ /
          G   H
           \ /
            I

Если я хочу убедиться, что ни один из классов не появляется более одного раза в любом из подклассов, какие базовые классы нужно маркировать virtual? Все они? Или достаточно использовать его только на тех классах, которые производятся непосредственно из класса, который может иметь несколько экземпляров (т.е. B, C, D, E и F; и G и H (но только с базовым классом E, а не с базовые классы D и F))?

4b9b3361

Ответ 1

Вы должны указать наследование virtual при наследовании от любого из классов A, B, C и E (которые находятся в верхней части алмаза).

class A;
class B: virtual A;
class C: virtual A;
class D: virtual B;
class E: virtual B, virtual C;
class F: virtual C;
class G:         D, virtual E;
class H: virtual E,         F;
class I:         G,         H;

Ответ 2

Я играл вместе программу, которая помогла вам изучить тонкости виртуальных баз. Он печатает иерархию классов под I в качестве орграфа, подходящего для графитиза (http://www.graphviz.org/). Там есть счетчик для каждого экземпляра, который также помогает вам понять порядок строительства. Здесь программа:

#include <stdio.h>
int counter=0; 



#define CONN2(N,X,Y)\
    int id; N() { id=counter++; }\
    void conn() \
    {\
        printf("%s_%d->%s_%d\n",#N,this->id,#X,((X*)this)->id); \
        printf("%s_%d->%s_%d\n",#N,this->id,#Y,((Y*)this)->id); \
        X::conn(); \
        Y::conn();\
    }
#define CONN1(N,X)\
    int id; N() { id=counter++; }\
    void conn() \
    {\
        printf("%s_%d->%s_%d\n",#N,this->id,#X,((X*)this)->id); \
        X::conn(); \
    }

struct A { int id; A() { id=counter++; } void conn() {} };
struct B : A { CONN1(B,A) };
struct C : A { CONN1(C,A)  };
struct D : B { CONN1(D,B) };
struct E : B,C { CONN2(E,B,C) };
struct F : C { CONN1(F,C) };
struct G : D,E { CONN2(G,D,E) };
struct H : E,F { CONN2(H,E,F) };
struct I : G,H { CONN2(I,G,H) };
int main()
{
    printf("digraph inh {\n");
    I i; 
    i.conn(); 
    printf("}\n");
}

Если я запустил это (g++ base.cc ; ./a.out >h.dot ; dot -Tpng -o o.png h.dot ; display o.png), я получаю типичное не виртуальное базовое дерево: alt text http://i34.tinypic.com/2ns6pt4.png

Добавление достаточной виртуальности...

struct B : virtual A { CONN1(B,A) };
struct C : virtual A { CONN1(C,A)  };
struct D : virtual B { CONN1(D,B) };
struct E : virtual B, virtual C { CONN2(E,B,C) };
struct F : virtual C { CONN1(F,C) };
struct G : D, virtual E { CONN2(G,D,E) };
struct H : virtual E,F { CONN2(H,E,F) };
struct I : G,H { CONN2(I,G,H) };

.. приводит к форме бриллианта (посмотрите на цифры, чтобы узнать порядок строительства!)

alt text http://i33.tinypic.com/xpa2l5.png

Но если вы делаете все базы виртуальными:

struct A { int id; A() { id=counter++; } void conn() {} };
struct B : virtual A { CONN1(B,A) };
struct C : virtual A { CONN1(C,A)  };
struct D : virtual B { CONN1(D,B) };
struct E : virtual B, virtual C { CONN2(E,B,C) };
struct F : virtual C { CONN1(F,C) };
struct G : virtual D, virtual E { CONN2(G,D,E) };
struct H : virtual E, virtual F { CONN2(H,E,F) };
struct I : virtual G,virtual H { CONN2(I,G,H) };

Вы получаете бриллиант с другим порядком инициализации:

alt text http://i33.tinypic.com/110dlj8.png

Удачи!

Ответ 3

Мое личное предложение - начать с B и C: virtual A, а затем продолжать добавлять до тех пор, пока компилятор перестанет жаловаться.

В реальности я бы сказал, что B и C: виртуальные A, G и H: виртуальные E и E: виртуальные B и C. Все остальные ссылки наследования могут быть обычным наследованием. Тем не менее, это чудовище потребовало бы шесть десятилетий, чтобы сделать виртуальный вызов.

Ответ 4

Если вы хотите убедиться, что объект верхнего класса в иерархии (I в вашем случае) содержит ровно один подобъект каждого родительского класса, вы должны найти все классы в своей иерархии, которые имеют более одного суперкласса и сделать эти классы виртуальными базами своих суперклассов. Что это.

В вашем случае классы A, B, C и E должны стать виртуальными базовыми классами каждый раз, когда вы наследуете их в этой иерархии.

Классы D, F, G и H не должны становиться виртуальными базовыми классами.

Ответ 5

Если вы хотите иметь только один "физический" экземпляр каждого типа для каждого экземпляра каждого типа (только один A, только один B и т.д.), вам просто нужно использовать виртуальное наследование каждый раз, когда вы используете наследование.

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

Ответ 6

Отредактировано. Я думал, что A является самым производным классом;)

@Luther ответ действительно круто, но вернемся к исходному вопросу:

НУЖНО использовать наследование virtual при наследовании от любого класса, из которого по крайней мере один другой класс наследуется в иерархии наследования (в диаграммах Лютера это означает, что по меньшей мере две стрелки указывают на класс).

Здесь это необязательно перед D, F, G и H, потому что из них происходит только один класс (и в данный момент ни один из них не имеет значения I).

Однако, если вы не знаете заранее, будет ли другой класс наследоваться от вашего базового класса, вы можете добавить в virtual в качестве меры предосторожности. Например, рекомендуется, чтобы класс Exception наследовал фактически от std::exception не только от самого Страустрапа.

Как отметил Лютер, он изменяет порядок создания экземпляров (и оказывает незначительное влияние на производительность), но я бы подумал, что любой дизайн, полагающийся на порядок построения, будет неправильным для начала. И точно так же, как точность: у вас все еще есть гарантии, что базовые классы инициализируются перед любым атрибутом производного класса и, следовательно, перед выполнением производного тела конструктора.

Ответ 7

В том, что нужно помнить, С++ хранит таблицу наследования. Чем больше вы добавляете виртуальные классы, тем дольше будет время компиляции (привязка), а более тяжелым будет время выполнения.

В общем случае, если можно избежать виртуального класса, вы можете заменить некоторые шаблоны или попытаться каким-то образом отделиться.