Где «виртуальное» ключевое слово необходимо в сложной иерархии множественного наследования? - PullRequest
12 голосов
/ 05 августа 2010

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

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

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

Ответы [ 7 ]

23 голосов
/ 05 августа 2010

Я вместе поиграл в программу, которая поможет вам изучить тонкости виртуальных баз.Он печатает иерархию классов в I в качестве орграфа, подходящего для graphiviz (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), я получаю типичное не виртуальное базовое дерево: альтернативный текст 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) };

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

альтернативный текст 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) };

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

альтернативный текст http://i33.tinypic.com/110dlj8.png

Веселитесь!

7 голосов
/ 05 августа 2010

Вы должны указать 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 голосов
/ 05 августа 2010

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

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

1 голос
/ 05 августа 2010

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

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

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

0 голосов
/ 29 октября 2015

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

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

0 голосов
/ 05 августа 2010

Отредактировано : я думал, что А был самым производным классом;)

@ Ответ Лютера действительно классный, но вернемся к первоначальному вопросу:

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

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

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

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

0 голосов
/ 05 августа 2010

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

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

...