Что такое виртуальный базовый класс в C++?
Я хочу знать, что такое "виртуальный базовый класс" и что он означает.
Позвольте мне показать пример:
class Foo
{
public:
void DoSomething() { /* ... */ }
};
class Bar : public virtual Foo
{
public:
void DoSpecific() { /* ... */ }
};
10 ответов
Виртуальные базовые классы, используемые в виртуальном наследовании, являются способом предотвращения появления нескольких "экземпляров" данного класса в иерархии наследования при использовании множественного наследования.
Рассмотрим следующий сценарий:
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's Foo() or C's 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
Надеюсь, что это поможет в качестве мини-резюме. Для получения дополнительной информации, прочитайте это и это. Хороший пример также доступен здесь.
О расположении памяти
Напомним, что проблема с 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
является неоднозначным, и это должно быть решено. Даже если это так, есть два m_iValues
внутри D
, так что лучше запомните это и обновите оба одновременно.
С виртуальным наследованием, изменение m_iValue
от D
все в порядке... но... допустим, что у вас есть D
, Через его C
интерфейс, вы прикрепили наблюдателя. И через его B
Интерфейс, вы обновляете классный массив, который имеет побочный эффект прямого изменения m_iValue
...
Как смена m_iValue
делается напрямую (без использования метода виртуального метода доступа), наблюдатель "прослушивает" через C
не будет вызван, потому что код, реализующий прослушивание, находится в C
, а также B
не знает об этом...
Заключение
Если у вас есть бриллиант в вашей иерархии, это означает, что у вас есть 95%, чтобы сделать что-то не так с указанной иерархией.
Для объяснения множественного наследования с помощью виртуальных баз требуется знание объектной модели C++. И объяснение темы лучше всего делать в статье, а не в поле для комментариев.
Наилучшим понятным объяснением, которое я нашел и которое разрешило все мои сомнения по этому вопросу, была эта статья: https://web.archive.org/web/20160413064252/http://www.phpcompiler.org/articles/virtualinheritance.html.
Вам действительно не нужно будет читать что-либо еще по этой теме (если вы не писатель компилятора) после прочтения этого...
Виртуальный базовый класс - это класс, создание которого невозможно, из него нельзя создать прямой объект.
Я думаю, что вы путаете две совершенно разные вещи. Виртуальное наследование - это не то же самое, что абстрактный класс. Виртуальное наследование изменяет поведение вызовов функций; иногда он разрешает вызовы функций, которые в противном случае были бы неоднозначными, иногда он откладывает обработку вызовов функций на класс, отличный от того, который можно ожидать в невиртуальном наследовании.
Я хотел бы добавить к добрым разъяснениям OJ.
Виртуальное наследство не обходится без цены. Как и со всеми виртуальными вещами, вы получаете удар по производительности. Этот хит производительности может быть менее элегантным.
Вместо того, чтобы разбивать алмаз путем виртуального извлечения, вы можете добавить еще один слой к алмазу, чтобы получить что-то вроде этого:
B
/ \
D11 D12
| |
D21 D22
\ /
DD
Ни один из классов не наследует виртуально, все наследуют публично. Классы D21 и D22 будут затем скрывать виртуальную функцию f (), которая неоднозначна для DD, возможно, объявив функцию частной. Каждый из них определил бы функцию-оболочку, f1 () и f2() соответственно, каждый из которых вызывал бы локальный класс (private) f (), таким образом разрешая конфликты. Класс DD вызывает f1 (), если он хочет D11::f() и f2(), если он хочет D12::f(). Если вы определите встроенные оболочки, вы, вероятно, получите около нуля накладных расходов.
Конечно, если вы можете изменить D11 и D12, вы можете сделать один и тот же трюк внутри этих классов, но часто это не так.
В дополнение к тому, что уже было сказано о множественном и виртуальном наследовании, в журнале д-ра Добба есть очень интересная статья: множественное наследование считается полезным
Пример использования бриллиантового наследования
В этом примере показано, как использовать виртуальный базовый класс в типичном сценарии: для решения проблемы наследования алмазов.
#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);
}
Обычное наследование
При типичном трехуровневом наследовании не-ромбовидного невиртуального наследования, когда вы создаете экземпляр нового наиболее производного объекта, вызывается new, и размер, требуемый для объекта, определяется компилятором из типа класса и передается в new.
новый имеет подпись:
_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
И звонит malloc
, возвращая указатель void
Затем он передается конструктору самого производного объекта, который немедленно вызовет средний конструктор, а затем средний конструктор немедленно вызовет базовый конструктор. Затем база сохраняет указатель на свою виртуальную таблицу в начале объекта, а затем его атрибуты после него. Затем он возвращается к среднему конструктору, который сохранит указатель виртуальной таблицы в том же месте, а затем свои атрибуты после атрибутов, которые были бы сохранены базовым конструктором. Он возвращается к наиболее производному конструктору, который сохраняет указатель на свою виртуальную таблицу в том же месте, а затем его атрибуты после атрибутов, которые были бы сохранены средним конструктором.
Поскольку указатель виртуальной таблицы перезаписывается, указатель виртуальной таблицы всегда оказывается одним из наиболее производных классов. Виртуальность распространяется в сторону самого производного класса, поэтому, если функция является виртуальной в среднем классе, она будет виртуальной в самом производном классе, но не в базовом классе. Если вы полиморфно приведете экземпляр наиболее производного класса к указателю на базовый класс, то компилятор не разрешит это косвенным вызовом виртуальной таблицы, а вместо этого вызовет функцию напрямую.A::function()
. Если функция является виртуальной для того типа, к которому вы ее применили, она разрешит вызов виртуальной таблицы, которая всегда будет таковой из наиболее производного класса. Если он не виртуальный для этого типа, он просто вызоветType::function()
и передайте ему указатель объекта, приведя его к типу.
Actually when I say pointer to its virtual table, it's actually always an offset of 16 into the virtual table.
vtable for Base:
.quad 0
.quad typeinfo for Base
.quad Base::CommonFunction()
.quad Base::VirtualFunction()
pointer is typically to the first function i.e.
mov edx, OFFSET FLAT:vtable for Base+16
virtual
is not required again in more-derived classes if it is virtual in a less-derived class because it propagates. But it can be used to show that the function is indeed a virtual function, without having to check the classes it inherits's type definitions.
override
is another compiler guard that says that this function is overriding something and if it isn't then throw a compiler error.
= 0
means that this is an abstract function
final
prevents a virtual function from being implemented again in a more derived class and will make sure that the virtual table of the most derived class contains the final function of that class.
= default
makes it explicit in documentation that the compiler will use the default implementation
= delete
give a compiler error if a call to this is attempted
Virtual Inheritance
Consider
class Base
{
int a = 1;
int b = 2;
public:
void virtual CommonFunction(){} ;
void virtual VirtualFunction(){} ;
};
class DerivedClass1: virtual public Base
{
int c = 3;
public:
void virtual DerivedCommonFunction(){} ;
void virtual VirtualFunction(){} ;
};
class DerivedClass2 : virtual public Base
{
int d = 4;
public:
//void virtual DerivedCommonFunction(){} ;
void virtual VirtualFunction(){} ;
void virtual DerivedCommonFunction2(){} ;
};
class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
{
int e = 5;
public:
void virtual DerivedDerivedCommonFunction(){} ;
void virtual VirtualFunction(){} ;
};
int main () {
DerivedDerivedClass* d = new DerivedDerivedClass;
d->VirtualFunction();
d->DerivedCommonFunction();
d->DerivedCommonFunction2();
d->DerivedDerivedCommonFunction();
((DerivedClass2*)d)->DerivedCommonFunction2();
((Base*)d)->VirtualFunction();
}
Without virtually inheriting the bass class you will get an object that looks like this:
Instead of this:
I.e. there will be 2 base objects.
In the virtual diamond inheritance situation above, after new is called, it calls the most derived constructor and in that constructor, it calls all 3 derived constructors passing offsets into its virtual table table, instead of calling just calling DerivedClass1::DerivedClass1()
and DerivedClass2::DerivedClass2()
and then those both calling Base::Base()
The following is all compiled in debug mode -O0 so there will be redundant assembly
main:
.LFB8:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
mov edi, 48 //pass size to new
call operator new(unsigned long) //call new
mov rbx, rax //move the address of the allocation to rbx
mov rdi, rbx //move it to rdi i.e. pass to the call
call DerivedDerivedClass::DerivedDerivedClass() [complete object constructor] //construct on this address
mov QWORD PTR [rbp-24], rbx //store the address of the object on the stack as d
DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]:
.LFB20:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
.LBB5:
mov rax, QWORD PTR [rbp-8] // object address now in rax
add rax, 32 //increment address by 32
mov rdi, rax // move object address+32 to rdi i.e. pass to call
call Base::Base() [base object constructor]
mov rax, QWORD PTR [rbp-8] //move object address to rax
mov edx, OFFSET FLAT:VTT for DerivedDerivedClass+8 //move address of VTT+8 to edx
mov rsi, rdx //pass VTT+8 address as 2nd parameter
mov rdi, rax //object address as first
call DerivedClass1::DerivedClass1() [base object constructor]
mov rax, QWORD PTR [rbp-8] //move object address to rax
add rax, 16 //increment object address by 16
mov edx, OFFSET FLAT:VTT for DerivedDerivedClass+24 //store address of VTT+24 in edx
mov rsi, rdx //pass address of VTT+24 as second parameter
mov rdi, rax //address of object as first
call DerivedClass2::DerivedClass2() [base object constructor]
mov edx, OFFSET FLAT:vtable for DerivedDerivedClass+24 //move this to edx
mov rax, QWORD PTR [rbp-8] // object address now in rax
mov QWORD PTR [rax], rdx. //store address of vtable for DerivedDerivedClass+24 at the start of the object
mov rax, QWORD PTR [rbp-8] // object address now in rax
add rax, 32 // increment object address by 32
mov edx, OFFSET FLAT:vtable for DerivedDerivedClass+120 //move this to edx
mov QWORD PTR [rax], rdx //store vtable for DerivedDerivedClass+120 at object+32 (Base)
mov edx, OFFSET FLAT:vtable for DerivedDerivedClass+72 //store this in edx
mov rax, QWORD PTR [rbp-8] //move object address to rax
mov QWORD PTR [rax+16], rdx //store vtable for DerivedDerivedClass+72 at object+16 (DerivedClass2)
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax+28], 5
.LBE5:
nop
leave
ret
It calls Base::Base()
with a pointer to the object offset 32. Base stores a pointer to its virtual table at the address it receives and its members after it.
Base::Base() [base object constructor]:
.LFB11:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi //stores address of object on stack (-O0)
.LBB2:
mov edx, OFFSET FLAT:vtable for Base+16 //puts vtable for Base+16 in edx
mov rax, QWORD PTR [rbp-8] //copies address of object from stack to rax
mov QWORD PTR [rax], rdx //stores it address of object
mov rax, QWORD PTR [rbp-8] //copies address of object on stack to rax again
mov DWORD PTR [rax+8], 1 //stores a = 1 in the object
mov rax, QWORD PTR [rbp-8] //junk from -O0
mov DWORD PTR [rax+12], 2 //stores b = 2 in the object
.LBE2:
nop
pop rbp
ret
DerivedDerivedClass::DerivedDerivedClass()
then calls DerivedClass1::DerivedClass1()
with a pointer to the object offset 0 and also passes the address of VTT for DerivedDerivedClass+8
DerivedClass1::DerivedClass1() [base object constructor]:
.LFB14:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi //address of object
mov QWORD PTR [rbp-16], rsi //address of VTT+8
.LBB3:
mov rax, QWORD PTR [rbp-16] //address of VTT+8 now in rax
mov rdx, QWORD PTR [rax] //address of DerivedClass1-in-DerivedDerivedClass+24 now in rdx
mov rax, QWORD PTR [rbp-8] //address of object now in rax
mov QWORD PTR [rax], rdx //store address of DerivedClass1-in-.. in the object
mov rax, QWORD PTR [rbp-8] // address of object now in rax
mov rax, QWORD PTR [rax] //address of DerivedClass1-in.. now implicitly in rax
sub rax, 24 //address of DerivedClass1-in-DerivedDerivedClass+0 now in rax
mov rax, QWORD PTR [rax] //value of 32 now in rax
mov rdx, rax // now in rdx
mov rax, QWORD PTR [rbp-8] //address of object now in rax
add rdx, rax //address of object+32 now in rdx
mov rax, QWORD PTR [rbp-16] //address of VTT+8 now in rax
mov rax, QWORD PTR [rax+8] //address of DerivedClass1-in-DerivedDerivedClass+72 (Base::CommonFunction()) now in rax
mov QWORD PTR [rdx], rax //store at address object+32 (offset to Base)
mov rax, QWORD PTR [rbp-8] //store address of object in rax, return
mov DWORD PTR [rax+8], 3 //store its attribute c = 3 in the object
.LBE3:
nop
pop rbp
ret
VTT for DerivedDerivedClass:
.quad vtable for DerivedDerivedClass+24
.quad construction vtable for DerivedClass1-in-DerivedDerivedClass+24
.quad construction vtable for DerivedClass1-in-DerivedDerivedClass+72
.quad construction vtable for DerivedClass2-in-DerivedDerivedClass+24
.quad construction vtable for DerivedClass2-in-DerivedDerivedClass+72
.quad vtable for DerivedDerivedClass+120
.quad vtable for DerivedDerivedClass+72
construction vtable for DerivedClass1-in-DerivedDerivedClass:
.quad 32
.quad 0
.quad typeinfo for DerivedClass1
.quad DerivedClass1::DerivedCommonFunction()
.quad DerivedClass1::VirtualFunction()
.quad -32
.quad 0
.quad -32
.quad typeinfo for DerivedClass1
.quad Base::CommonFunction()
.quad virtual thunk to DerivedClass1::VirtualFunction()
construction vtable for DerivedClass2-in-DerivedDerivedClass:
.quad 16
.quad 0
.quad typeinfo for DerivedClass2
.quad DerivedClass2::VirtualFunction()
.quad DerivedClass2::DerivedCommonFunction2()
.quad -16
.quad 0
.quad -16
.quad typeinfo for DerivedClass2
.quad Base::CommonFunction()
.quad virtual thunk to DerivedClass2::VirtualFunction()
vtable for DerivedDerivedClass:
.quad 32
.quad 0
.quad typeinfo for DerivedDerivedClass
.quad DerivedClass1::DerivedCommonFunction()
.quad DerivedDerivedClass::VirtualFunction()
.quad DerivedDerivedClass::DerivedDerivedCommonFunction()
.quad 16
.quad -16
.quad typeinfo for DerivedDerivedClass
.quad non-virtual thunk to DerivedDerivedClass::VirtualFunction()
.quad DerivedClass2::DerivedCommonFunction2()
.quad -32
.quad 0
.quad -32
.quad typeinfo for DerivedDerivedClass
.quad Base::CommonFunction()
.quad virtual thunk to DerivedDerivedClass::VirtualFunction()
virtual thunk to DerivedClass1::VirtualFunction():
mov r10, QWORD PTR [rdi]
add rdi, QWORD PTR [r10-32]
jmp .LTHUNK0
virtual thunk to DerivedClass2::VirtualFunction():
mov r10, QWORD PTR [rdi]
add rdi, QWORD PTR [r10-32]
jmp .LTHUNK1
virtual thunk to DerivedDerivedClass::VirtualFunction():
mov r10, QWORD PTR [rdi]
add rdi, QWORD PTR [r10-32]
jmp .LTHUNK2
non-virtual thunk to DerivedDerivedClass::VirtualFunction():
sub rdi, 16
jmp .LTHUNK3
.set .LTHUNK0,DerivedClass1::VirtualFunction()
.set .LTHUNK1,DerivedClass2::VirtualFunction()
.set .LTHUNK2,DerivedDerivedClass::VirtualFunction()
.set .LTHUNK3,DerivedDerivedClass::VirtualFunction()
DerivedDerivedClass::DerivedDerivedClass()
then passes the address of the object+16 and the address of VTT for DerivedDerivedClass+24
to DerivedClass2::DerivedClass2()
whose assembly is identical to DerivedClass1::DerivedClass1()
except for the line mov DWORD PTR [rax+8], 3
which obviously has a 4 instead of 3 for d = 4
.
After this, it replaces all 3 virtual table pointers in the object with pointers to offsets in DerivedDerivedClass
's vtable to the representation for that class.
d->VirtualFunction();
:
mov rax, QWORD PTR [rbp-24] //store pointer to virtual table in rax
mov rax, QWORD PTR [rax] //dereference and store in rax
add rax, 8 // call the 2nd function in the table
mov rdx, QWORD PTR [rax] //dereference
mov rax, QWORD PTR [rbp-24]
mov rdi, rax
call rdx
d->DerivedCommonFunction();
:
mov rax, QWORD PTR [rbp-24]
mov rdx, QWORD PTR [rbp-24]
mov rdx, QWORD PTR [rdx]
mov rdx, QWORD PTR [rdx]
mov rdi, rax
call rdx
d->DerivedCommonFunction2();
:
mov rax, QWORD PTR [rbp-24]
lea rdx, [rax+16]
mov rax, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rax+16]
add rax, 8
mov rax, QWORD PTR [rax]
mov rdi, rdx
call rax
d->DerivedDerivedCommonFunction();
:
mov rax, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rax]
add rax, 16
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-24]
mov rdi, rax
call rdx
((DerivedClass2*)d)->DerivedCommonFunction2();
:
cmp QWORD PTR [rbp-24], 0
je .L14
mov rax, QWORD PTR [rbp-24]
add rax, 16
jmp .L15
.L14:
mov eax, 0
.L15:
cmp QWORD PTR [rbp-24], 0
cmp QWORD PTR [rbp-24], 0
je .L18
mov rdx, QWORD PTR [rbp-24]
add rdx, 16
jmp .L19
.L18:
mov edx, 0
.L19:
mov rdx, QWORD PTR [rdx]
add rdx, 8
mov rdx, QWORD PTR [rdx]
mov rdi, rax
call rdx
((Base*)d)->VirtualFunction();
:
cmp QWORD PTR [rbp-24], 0
je .L20
mov rax, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rax]
sub rax, 24
mov rax, QWORD PTR [rax]
mov rdx, rax
mov rax, QWORD PTR [rbp-24]
add rax, rdx
jmp .L21
.L20:
mov eax, 0
.L21:
cmp QWORD PTR [rbp-24], 0
cmp QWORD PTR [rbp-24], 0
je .L24
mov rdx, QWORD PTR [rbp-24]
mov rdx, QWORD PTR [rdx]
sub rdx, 24
mov rdx, QWORD PTR [rdx]
mov rcx, rdx
mov rdx, QWORD PTR [rbp-24]
add rdx, rcx
jmp .L25
.L24:
mov edx, 0
.L25:
mov rdx, QWORD PTR [rdx]
add rdx, 8
mov rdx, QWORD PTR [rdx]
mov rdi, rax
call rdx
Это означает, что вызов виртуальной функции будет перенаправлен в "правильный" класс.
C++ FAQ Lite FTW.
Короче говоря, он часто используется в сценариях множественного наследования, где формируется "алмазная" иерархия. Виртуальное наследование разрушит неоднозначность, созданную в нижнем классе, когда вы вызываете функцию в этом классе, и функция должна быть преобразована в класс D1 или D2 выше этого нижнего класса. Смотрите пункт FAQ для диаграммы и деталей.
Это также используется в делегировании сестры, мощная функция (хотя и не для слабонервных). Смотрите этот FAQ.
Также см. Пункт 40 в Effective C++, 3-е издание (43 во 2-м издании).
Ты немного сбиваешь с толку. Я не знаю, смешиваете ли вы некоторые понятия.
У вас нет виртуального базового класса в вашем OP. У вас просто есть базовый класс.
Вы сделали виртуальное наследование. Это обычно используется в множественном наследовании, так что несколько производных классов используют члены базового класса, не воспроизводя их.
Базовый класс с чисто виртуальной функцией не создается. для этого требуется синтаксис, который использует Пол. Обычно он используется для того, чтобы производные классы определяли эти функции.
Я не хочу больше об этом объяснять, потому что я не совсем понимаю, что вы спрашиваете.
Виртуальные классы - это не виртуальное наследование. Виртуальные классы, которые вы не можете создать, виртуальное наследование - это совсем другое.
Википедия описывает это лучше, чем я. http://en.wikipedia.org/wiki/Virtual_inheritance