Что такое виртуальный базовый класс в C ++?

364

Я хочу знать, что такое "виртуальный базовый класс" и что он означает.

Позвольте мне показать пример:

class Foo
{
public:
    void DoSomething() { /* ... */ }
};

class Bar : public virtual Foo
{
public:
    void DoSpecific() { /* ... */ }
};
  • 0
    мы должны использовать виртуальные базовые классы в «множественном наследовании», потому что, если класс A имеет переменную-член int a, а класс B также имеет член int a, а класс c наследует класс A и B, как мы решаем, какое «a» использовать?
  • 2
    @NamitSinha нет, виртуальное наследование не решает эту проблему. Член А был бы неоднозначным в любом случае
Теги:
virtual-inheritance

11 ответов

510
Лучший ответ

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

Рассмотрим следующий сценарий:

class A { public: void Foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};

Вышеупомянутая иерархия классов приводит к "страшному алмазу", который выглядит следующим образом:

  A
 / \
B   C
 \ /
  D

Экземпляр D будет состоять из B, который включает A и C, который также включает A. Таким образом, у вас есть два "экземпляра" (из-за лучшего выражения) A.

Когда у вас есть этот сценарий, у вас есть возможность двусмысленности. Что происходит, когда вы это делаете:

D d;
d.Foo(); // is this B Foo() or C Foo() ??

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

class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};

Это означает, что в иерархии есть только один экземпляр A. Следовательно

D d;
d.Foo(); // no longer ambiguous

Надеюсь, что это поможет в мини-резюме. Для получения дополнительной информации прочитайте this и this. Хороший пример также доступен здесь.

  • 17
    Означает ли это, что мы всегда должны использовать виртуальные ????????????
  • 6
    @ Bohdan нет, это не так :)
Показать ещё 15 комментариев
208

О макете памяти

В качестве побочной заметки проблема с Dreaded Diamond заключается в том, что базовый класс присутствует несколько раз. Таким образом, с регулярным наследованием вы полагаете, что у вас есть:

  A
 / \
B   C
 \ /
  D

Но в макете памяти у вас есть:

A   A
|   |
B   C
 \ /
  D

Это объясняет, почему при вызове D::foo() вас есть проблема двусмысленности. Но настоящая проблема возникает, когда вы хотите использовать переменную-член A Например, скажем, у нас есть:

class A
{
    public :
       foo() ;
       int m_iValue ;
} ;

Когда вы попытаетесь получить доступ к m_iValue из D, компилятор будет протестовать, потому что в иерархии он увидит два m_iValue, а не один. И если вы измените один, скажем, B::m_iValue (то есть родитель A::m_iValue для B), C::m_iValue не будет изменен (это родительский A::m_iValue для C).

Это то, где виртуальное наследование подходит, как и в случае с ним, вы вернетесь к подлинному макету бриллианта, используя не только один метод foo(), но и один и только один m_iValue.

Что может пойти не так?

Представить:

  • A имеет некоторую базовую функцию.
  • B добавляет к нему какой-то классный массив данных (например)
  • C добавляет к нему некоторую интересную функцию, такую как шаблон наблюдателя (например, на m_iValue).
  • D наследуется от B и C и, следовательно, от A

При нормальном наследовании изменение m_iValue из D неоднозначно, и это необходимо устранить. Даже если это так, внутри D есть два m_iValues, поэтому вам лучше запомнить это и одновременно обновить два.

С виртуальным наследованием изменение m_iValue из D в порядке... Но... Пусть говорят, что у вас есть D Через свой интерфейс C вы подключили наблюдателя. И через свой интерфейс B вы обновляете классный массив, который имеет побочный эффект непосредственно изменения m_iValue...

Поскольку изменение m_iValue выполняется напрямую (без использования метода виртуального доступа), наблюдатель, "прослушивающий" через C, не будет вызываться, потому что код, реализующий прослушивание, находится на C, а B не знает об этом.,

Заключение

Если у вас есть бриллиант в вашей иерархии, это означает, что у вас есть 95%, чтобы сделать что-то неправильно с указанной иерархией.

  • 0
    Ваше «что может пойти не так» связано с прямым доступом к базовому члену, а не с множественным наследованием. Избавьтесь от «B», и у вас возникнет та же проблема. Основное правило: «если оно не является частным, оно должно быть виртуальным», то устраняет проблему. M_iValue не является виртуальным и поэтому должно быть частным
  • 3
    @ Крис Додд: Не совсем. То, что происходит с m_iValue, произошло бы с любым символом ( например, typedef, переменная-член, функция-член, приведение к базовому классу и т . Д. ). Это действительно проблема множественного наследования, проблема, которую пользователи должны знать, чтобы правильно использовать множественное наследование, вместо того, чтобы идти по пути Java, и сделать вывод: «Многократное наследование - это 100% зло, давайте сделаем это с интерфейсами».
Показать ещё 5 комментариев
30

Объяснение множественного наследования с виртуальными базами требует знания объектной модели С++. И разъяснение темы ясно лучше всего сделать в статье, а не в блоке комментариев.

Лучшее, читаемое объяснение, которое я нашел, решил все мои сомнения по этому вопросу: http://www.phpcompiler.org/articles/virtualinheritance.html

Вам действительно не нужно читать что-либо по этой теме (если вы не являетесь компилятором) после прочтения этого...

10

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

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

6

Я хотел бы добавить к разъяснениям OJ.

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

Вместо того, чтобы разбить алмаз, получив фактически, вы можете добавить еще один слой к алмазу, чтобы получить что-то вроде этого:

   B
  / \
D11 D12
 |   |
D21 D22
 \   /
  DD

Ни один из классов наследует фактически, все наследуют публично. Классы D21 и D22 затем скроют виртуальную функцию f(), которая неоднозначна для DD, возможно, объявив функцию закрытой. Каждый из них определяет функцию-обертку, f1() и f2() соответственно, каждый вызывающий класс-локальный (частный) f(), тем самым разрешая конфликты. Класс DD вызывает f1(), если он хочет D11:: f() и f2(), если он хочет D12:: f(). Если вы определяете встроенные обертки, вы, вероятно, получите нулевые служебные данные.

Конечно, если вы можете изменить D11 и D12, вы можете сделать тот же трюк внутри этих классов, но часто это не так.

  • 2
    Это не вопрос более-менее элегантного или разрешения неясностей (для этого всегда можно использовать явные спецификации xxx ::). В случае невиртуального наследования каждый экземпляр класса DD имеет два независимых экземпляра B. Как только у класса есть один элемент не статических данных, виртуальное и не виртуальное наследование отличаются не только синтаксисом.
  • 0
    @ user3489112 Как только ... ничего. Виртуальное и не виртуальное наследование отличаются семантически, точка.
4

В дополнение к тому, что уже было сказано о множественном и виртуальном наследовании, существует очень интересная статья о журнале Dr Dobb: Многократное наследование Полезные

1

Пример использования наследования с наследованием алмазов

В этом примере показано, как использовать виртуальный базовый класс в типичном сценарии: разрешить наследование алмазов.

#include <cassert>

class A {
    public:
        A(){}
        A(int i) : i(i) {}
        int i;
        virtual int f() = 0;
        virtual int g() = 0;
        virtual int h() = 0;
};

class B : public virtual A {
    public:
        B(int j) : j(j) {}
        int j;
        virtual int f() { return this->i + this->j; }
};

class C : public virtual A {
    public:
        C(int k) : k(k) {}
        int k;
        virtual int g() { return this->i + this->k; }
};

class D : public B, public C {
    public:
        D(int i, int j, int k) : A(i), B(j), C(k) {}
        virtual int h() { return this->i + this->j + this->k; }
};

int main() {
    D d = D(1, 2, 4);
    assert(d.f() == 3);
    assert(d.g() == 5);
    assert(d.h() == 7);
}
  • 2
    assert(A::aDefault == 0); из основной функции выдает ошибку компиляции: aDefault is not a member of A использующим gcc 5.4.0. Что это должно делать?
  • 0
    @SebTu ах спасибо, просто что-то, что я забыл удалить из копии вставить, удалил это сейчас. Пример все еще должен быть значимым без него.
1

Это означает, что вызов виртуальной функции будет перенаправлен в "правый" класс.

С++ FAQ Lite FTW.

Короче говоря, он часто используется в сценариях с множественным наследованием, где формируется иерархия "алмазов". Виртуальное наследование затем нарушит неоднозначность, созданную в нижнем классе, когда вы вызываете функцию в этом классе, и функция должна быть разрешена либо классу D1, либо D2 выше этого нижнего класса. См. пункт часто задаваемых вопросов для диаграммы и деталей.

Он также используется в сестринской делегации, мощная функция (хотя и не для слабонервных). См. этот FAQ.

Также см. пункт 40 в "Эффективном выпуске С++ 3-е издание" (43 в 2-м выпуске).

1

Ты немного запутался. Я не знаю, смешиваете ли вы некоторые понятия.

У вас нет виртуального базового класса в вашем OP. У вас просто базовый класс.

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

Базовый класс с чистой виртуальной функцией не создается. для этого требуется синтаксис, который получает Пол. Он обычно используется так, чтобы производные классы определяли эти функции.

Я не хочу больше об этом объяснять, потому что я не полностью понимаю то, что вы просите.

  • 1
    «Базовый класс», который используется в виртуальном наследовании, становится «виртуальным базовым классом» (в контексте этого точного наследования).
0

Виртуальные классы не совпадают с виртуальным наследованием. Виртуальные классы, которые вы не можете создать, виртуальное наследование - это совсем другое.

Википедия описывает это лучше, чем я могу. http://en.wikipedia.org/wiki/Virtual_inheritance

  • 4
    В C ++ нет такого понятия, как «виртуальные классы». Однако существуют «виртуальные базовые классы», которые являются «виртуальными» в отношении данного наследования. То, что вы ссылаетесь, это то, что официально называется «абстрактные классы».
  • 0
    @LucHermitte, в C ++ есть определённые виртуальные классы. Проверьте это: en.wikipedia.org/wiki/Virtual_class .
Показать ещё 1 комментарий
-3

Это один из моих интервью; почти каждый считает, что "виртуальный базовый класс" означает "класс с чистыми виртуальными спецификаторами", как один из ответов здесь. Это неверно. В дизайне OO вы часто видите конкретные классы с несколькими интерфейсами. Существует концепция, называемая "реализацией на основе политик", которая реализует некоторые или все интерфейсы, вы можете просто подключиться к реализации и повторного использования. Вы не слышите о реализации на основе политик в контексте программирования OO, потому что Java и С# не поддерживают его. Но на самом деле Python и С++ делают.

Похоже на это Интерфейсы:

struct ICanFly {
    virtual ~ICanFly() = default;
    virtual void fly() = 0;
};
struct IStrong {
    virtual ~IStrong() = default;
    virtual void powerful() =0 ;
};
struct IUncanny {
    virtual ~IUncanny() = default;
    virtual void predict() = 0;
};
struct ISuperHero :virtual ICanFly 
    ,virtual IStrong , virtual IUncanny {};

Реализация запаса:

struct Bird: virtual ICanFly {
    void fly()override { flapwings(); } 
};
struct Plane :virtual ICanFly {
    void fly()override { movefast(); }
};
struct Rocket :virtual ICanFly {
    void fly()override { generatethrust(); }
};
struct Locomotive :virtual IStrong {
    void powerful()override { chug(); }
};
struct BigDam :virtual IStrong {
    void powerful()override { holdback(); }
};
struct Psychic :virtual IUncanny {
    void predict()override { useintuition(); }
};
struct AI :virtual IUncanny {
    void predict()override { compute(); }
};

И некоторые реализации, использующие классы запасов

struct SuperMan final :protected AI, protected Rocket, protected BigDam, ISuperHero
{};
struct WonderWoman final :
     protected Psychic , protected Plane, ISuperHero
{
    void powerful()final { lasso(); }
};

Использование защищенного здесь запрещает неявные преобразования к классам реализации, то есть реализация не моделирует отношения "is-a". Это делают только интерфейсы. (Публичный является последним, главным образом из-за упорядочения правил наследования С++, правильное большинство наследования является "доминирующим" )

Интерфейсы часто имеют несколько функций, я действительно могу это сделать

struct IFooBar {
    virtual ~IFooBar() = default;
    virtual void foo() = 0;
    virtual void bar() = 0;
};

struct Foo1:virtual IFooBar {
    void foo()override  {}
};
struct Foo1 :virtual IFooBar {
    void foo() override {}
};
struct Bar1 :virtual IFooBar {
    void bar() override {}
};
struct Bar2 :virtual IFooBar {
    void bar() override {}
};


struct Final11 final :protected Bar1, protected Foo1,virtual IFooBar {};
struct Final21 final :protected Bar2, protected Foo1,virtual IFooBar {};

Я использую этот стиль для большого количества проектов. "Конечные" спецификаторы здесь помогают оптимизатору в разрешении, где он может безопасно девиртуализировать. Девиртуализация в С++, часть 1, приводящая к классу, который выполняет те же действия, что и все шаблоны.

inline void dofoo(IFooBar const&d) {
    d.foo();
}
template<class X>
void dofooT(X const&d) {
    d.foo();
}

Final11 f;
dofooT(f);//compiler knows the final type here 
dofoo(f);//it does here too
//no difference

IFooBar const& g=dynamic_cast<IFooBar& >(someobject);
dofooT(f);//compiler doesn't know the final type here, calls virtual  
dofoo(f);//it doesnt here either
//no difference

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

В С++ "политическая реализация" обычно выполняется с использованием шаблонов, а не с интерфейсами с виртуальными функциями, а шаблоны вводят много связей на различные части системы. Microsoft ATL - отличный пример использования виртуальных функций, но не виртуальных баз, поскольку механизм COM был реализован отдельно от разрешения динамического типа в компиляторе MSVC (он фактически предшествует стандарту С++ здесь).

Ещё вопросы

Сообщество Overcoder
Наверх
Меню