Minimal class data definitions have been chosen for the
purposes of illustrating the various features.
In this paper, the short identifiers signify the following:
T
: type [class],
t
: variable of type T
,
f
: function[-member],
d
: data-member.
All data members have conveniently chosen to be of the size of a pointer
(void*
, int
, in 'normal' mode [e.g.
IPL32
]).
The assembly samples are chosen to be x86, 32 bit.
C++ object layout is compiler dependent; the samples reflect
only one of the possible approaches.
virtual function call
Consider the following simple class, and a call on a reference of an
instance of it
[note that the actual T
code is not needed]
:
class T {
public :
T();
virtual int f();
private :
int d;
};
void x(T& t) {
t.f();
}
The function x
is e.g. translated to the following
assembly code
:
void __cdecl x(class T&):
mov ecx,[esp+4] ;t
mov eax,[ecx] ;vftable
jmp [eax] ;f
The call to f
is done indirectly, via the type's
virtual function table.
&t
begins with a pointer to T
's virtual
function table (virtual method table, vftable
,
vtbl
); the first member of that vftable
is a
pointer to f
[the first virtual method].
The object layout looks like this
:
object vtbl
+---------------+ +---------------+
| vtbl --+--->| f |
+---------------+ +---------------+
| d |
+---------------+
Consider the above class definition without the
virtual
modifier on f
.
x
will e.g. translate to
:
void __cdecl x(class T&):
mov ecx,[esp+4] ;t
jmp (public: int __thiscall T::f(void)) ;f
t.f()
is translated to a direct call.
The reference variable t
is not
polymorphic in this case.
Advantages of such non-polymorphic calls are the possibility to
inline the called code [done by the optimizer], to possibly
make code run faster.
In this case the object layout looks like [i.e. it's a C
struct
without any special C++ features]
:
object
+---------------+
| d |
+---------------+
A similar direct call is generated if the reference modifier
&
is left away in x
's
parameter definition
:
void __cdecl x(class T):
lea ecx,[esp+4] ;&t
jmp (public: virtual int __thiscall T::f(void)) ;f
In this case the compiler knows &t
's exact
type (since it is no reference), and uses this knowledge for
possible higher performance.
virtual multiple inheritance
Consider classes T1
and T2
to be similar
defined as T
above (all identifiers have a number suffix
though).
T3
virtually inherits from T1
and
T2
(in that order), overwrites
T1::f1
and T2::f2
, and adds a new
function-member f3
.
x3
is used to illustrate upcasting (widening),
i.e. casting to base classes
:
class T3 : public virtual T1, public virtual T2 {
public :
T3();
virtual int f1();
virtual int f2();
virtual int f3();
private :
int d3;
};
void x1(T1& t);
void x2(T2& t);
void x3(T3& t) {
x1(t);
x2(t);
}
The x3
is e.g. translated to the following code
:
void __cdecl x3(class T3&):
push esi
mov esi,[esp+8] ;t
mov eax,[esi+4] ;vbtable
mov ecx,[eax+4]
lea eax,[ecx+esi+4] ;(T1&)t [via vbtable]
push eax
call (void __cdecl x1(class T1&))
add esp,4
mov edx,[esi+4] ;vbtable
mov eax,[edx+8]
lea eax,[eax+esi+4] ;(T2&)t [via vbtable]
push eax
call (void __cdecl x2(class T2&))
add esp,4
pop esi
ret
Casting to both T1&
and T2&
changes
the this
pointer (+ 16 bytes, + 28 bytes), which is done
via a virtual base class table (vbtable
,
vbtbl
).
The layout of a virtual multiple inheritance object may look like this
:
object vtbl
+---------------+ +---------------+
t3: | vtbl --+--->| f3 | vbtbl
+---------------+ +---------------+ +---------------+
| vbtbl --+------------------------>| t3 offset, -4 |
+---------------+ +---------------+
| d3 | | t1 offset, 12 |
+---------------+ +---------------+
| 0 | vtbl1 | t2 offset, 24 |
+---------------+ +---------------+ +---------------+
t1: | vtbl1 --+--->| f1 thunk |
+---------------+ +---------------+
| d1 |
+---------------+
| 0 | vtbl2
+---------------+ +---------------+
t2: | vtbl2 --+--->| f2 thunk |
+---------------+ +---------------+
| d2 |
+---------------+
The object parts (specially t1 and t3) are not glued together;
virtual derivations (T3) have additionally a virtual base
class table; base classes are prepended by an additional
index, which are used in the implemented thunks.
The vbtable
can be overwritten at derived object
construction time.
Virtual inherited classes constructors implement an additional parameter
to suppress [normal] base class construction, which is required to
realize common base class parts as single instances.
Object setup code is e.g.
:
public: __thiscall T3::T3(void):
mov eax,[esp+4]
push esi
test eax,eax
mov esi,ecx ;this
je 1f
lea ecx,[esi+10h] ;(T1&)t
mov [esi+4],(const T3::`vbtable')
call (public: __thiscall T1::T1(void))
lea ecx,[esi+1Ch] ;(T2&)t
call (public: __thiscall T2::T2(void))
1: ;base classes constructed
mov eax,[esi+4] ;vbtable
mov [esi+8],0 ;d3
mov [esi],(const T3::`vftable'{for `T3'})
mov ecx,[eax+4]
mov [ecx+esi+4],(const T3::`vftable'{for `T1'}) ;(T1&)t [via vbtable]
mov edx,[esi+4]
mov eax,[edx+8]
mov [eax+esi+4],(const T3::`vftable'{for `T2'}) ;(T2&)t [via vbtable]
mov ecx,[esi+4]
mov eax,[ecx+4]
lea edx,[eax-0Ch]
mov [esi+eax],edx
mov eax,[esi+4]
mov eax,[eax+8]
lea ecx,[eax-18h]
mov [esi+eax],ecx
mov eax,esi
pop esi
ret 4
[thunk]:public: virtual int __thiscall T3::f1`vtordisp{-4,0}'(void):
sub ecx,[ecx-4]
jmp (public: virtual int __thiscall T3::f1(void))
[thunk]:public: virtual int __thiscall T3::f2`vtordisp{-4,0}'(void):
sub ecx,[ecx-4]
jmp (public: virtual int __thiscall T3::f2(void))
const T3::`vftable'{for `T1'}:
([thunk]:public: virtual int __thiscall T3::f1`vtordisp{-4,0}'(void))
const T3::`vftable'{for `T2'}:
([thunk]:public: virtual int __thiscall T3::f2`vtordisp{-4,0}'(void))
const T3::`vftable'{for `T3'}:
(public: virtual int __thiscall T3::f3(void))
const T3::`vbtable':
-4 ;t3 offset
12 ;t1 offset
24 ;t2 offset
Interesting are the differences between the setup of
this
in T3::f1
, T3::f2
and
T3::f3
:
in f1
, d3
is referenced by
[ecx-8]
(negative offset);
in f2
, d3
is referenced by
[ecx-14h]
(also a negative offset),
in f3
, d3
is referenced by
[ecx+8]
.
Consider non-virtual inheritance, leaving away the
virtual
base class modifiers.
The x3
is e.g. translated to the following code
:
void __cdecl x3(class T3&):
push esi
mov esi,[esp+8] ;t
push esi ;(T1&)t
call (void __cdecl x1(class T1&))
add esp,4
lea eax,[esi+8] ;(T2&)t
push eax
call (void __cdecl x2(class T2&))
add esp,4
pop esi
ret
Casting t
to T2
changes the this
pointer (+ 8 bytes), casting to T1
won't.
This requires an additional virtual function table for the
T2
part of the object.
The first vftable includes the new function(s).
Non-virtual multiple inheritance leads to simpler, more compact
objects, which however lack some flexibility required with e.g.
diamond-shaped class hierarchies.
The layout of a non-virtual multiple inheritance object may look like
this
:
object vtbl
+---------------+ +---------------+
t1,t3: | vtbl --+--->| f1 |
+---------------+ +---------------+
| d1 | | f3 | vtbl2
+---------------+ +---------------+ +---------------+
t2: | vtbl2 --+------------------------>| f2 |
+---------------+ +---------------+
| d2 |
+---------------+
| d3 |
+---------------+
Object setup code is e.g.
:
public: __thiscall T3::T3(void):
push esi
push edi
mov esi,ecx ;this
call (public: __thiscall T1::T1(void)) ;(T1&)t == t
lea edi,[esi+8] ;(T2&)t
mov ecx,edi
call (public: __thiscall T2::T2(void))
mov [edi],(const T3::`vftable'{for `T2'}) ;vtbl2
mov [esi+10h],0 ;d3
mov [esi],(const T3::`vftable'{for `T1'}) ;vtbl
mov eax,esi
pop edi
pop esi
ret
const T3::`vftable'{for `T1'}:
(public: virtual int __thiscall T3::f1(void))
(public: virtual int __thiscall T3::f3(void))
const T3::`vftable'{for `T2'}:
(public: virtual int __thiscall T3::f2(void))
Interesting are the differences between the setup of
this
in T3::f1
and T3::f2
:
in f1
, d3
is referenced by
[ecx+10h]
;
in f2
, d3
is referenced by
[ecx+8]
.