preamble
The C++ object model is a common, and complex, topic, and this article is based on theItanium C++ ABIThe object model in several simple C++ inheritance scenarios, especially in the presence of dummy functions, is introduced through program practice, and the memory layout is visualized in a diagram.
The program build environment shown in this article is Ubuntu, glibc 2.24, gcc 6.3.0. Since both the clang and gcc compilers are based on the Itanium C++ ABI (for more information refer to thegcc ABI policy), so the object model presented in this paper is largely applicable to clang-compiled programs as well.
Introduction to the Virtual Function Table
virtual function table layout (VFTML)
Classes that contain virtual functions for which the compiler adds aVirtual function table (vptr))。
Verify the memory layout of a class containing a dummy function with the following program, which is very simple, defining only the constructor, dummy destructor, and an int member variable.
// class Base_C { public: Base_C(); virtual ~Base_C(); private: int baseC; }; // Base_C::Base_C() { } Base_C::~Base_C() { }
The gcc compiler can be configured with the-fdump-class-hierarchy
parameter to see the memory layout of the class. The following information can be obtained:
// g++ -O0 -std=c++11 -fdump-class-hierarchy Vtable for Base_C Base_C::_ZTV6Base_C: 4u entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTI6Base_C) 16 (int (*)(...))Base_C::~Base_C 24 (int (*)(...))Base_C::~Base_C Class Base_C size=16 align=8 base size=12 base align=8 Base_C (0x0x7fb8e9185660) 0 vptr=((& Base_C::_ZTV6Base_C) + 16u)
generic term for sth.Base_C
In terms of the definition of the class, the space occupied by the class consists of aVirtual function table pointer vptrand ainteger variable. Due to memory alignment, classes take up16 bytes。
Next, look at the virtual function table, there are 4 entries in the table, each entry is a function pointer to a specific virtual function, so each entry takes up 8 bytes (the size of the pointer) to compile on the machine being tested.
Notice that you see two virtual destructors in the table, which is actually mandated by the Itanium C++ ABI:
The entries for virtual destructors are actually pairs of entries.
The first destructor, called the complete object destructor, performs the destruction without calling delete() on the object.
The second destructor, called the deleting destructor, calls delete() after destroying the object.
Both destroy any virtual bases; a separate, non-virtual function, called the base object destructor,
performs destruction of the object but not its virtual base subobjects, and does not call delete().
The virtual destructor occupies two entries in the table of virtual functions, namelycomplete object destructor
cap (a poem)deleting destructor
。
In addition to the destructor, the table of virtual functions has two other entries, immediately after the destructortypeinfo
Pointer to a typeinfo object for runtime type identification (RTTI).
The first entry may seem unfamiliar and isoffset
The offset stores the displacement from the current location of the virtual table pointer (vtable pointer) to the top of the object. Both entries are described in detail in the ABI documentation:
// typeinfo pointer
The typeinfo pointer points to the typeinfo object used for RTTI. It is always present.
All entries in each of the virtual tables for a given class must point to the same typeinfo object.
A correct implementation of typeinfo equality is to check pointer equality, except for pointers (directly or indirectly) to incomplete types.
The typeinfo pointer is a valid pointer for polymorphic classes, . those with virtual functions, and is zero for non-polymorphic classes.
// offset offset The offset to top holds the displacement to the top of the object from the location within the object of the virtual table pointer that addresses this virtual table, as a ptrdiff_t. It is always present. The offset provides a way to find the top of the object from any base subobject with a virtual table pointer. This is necessary for dynamic_cast<void*> in particular. In a complete object virtual table, and therefore in all of its primary base virtual tables, the value of this offset will be zero. For the secondary virtual tables of other non-virtual bases, and of many virtual bases, it will be negative. Only in some construction virtual tables will some virtual base virtual tables have positive offsets, due to a different ordering of the virtual bases in the full object than in the subobject's standalone layout.
Another thing to note:vptr=((& Base_C::_ZTV6Base_C) + 16u)
, although there are four entries in the virtual function table, theThe pointer to vptr does not actually point to the start of the table, but rather to the location of the first virtual function。
Base_C
The memory layout is shown below:
The C++ Object Model under Inheritance
C++ Object Model under Single Inheritance
First, look at an example of a single inheritance scenario:
// The implementation part of the class is omitted here class Base_C { public: Base_C(); virtual ~Base_C(); private: int baseC; }; class Base_D : public Base_C { public: Base_D(int i); virtual ~Base_D(); virtual void add(void) { cout << "Base_D::add()..." << endl; } virtual void print(void); private: int baseD; }; class Derive_single : public Base_D { public: Derive_single(int d); void print(void) override; virtual void Derive_single_print(); private: int Derive_singleValue; };
In a single-inheritance scenario, the derived class has one and only one virtual table (copying the virtual table of the base class), while the virtual functions that are overridden in the derived class, will have their virtual tables in theOverriding the original function in the virtual function table,New virtual functions added to derived classes will also be appended to the end of the virtual function table。
In terms of overall memory layout, theNew non-static member variables added to a derived class are also appended to the base class' member variables。
The print class memory layout is as follows:
Vtable for Derive_single Derive_single::_ZTV13Derive_single: 7u entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTI13Derive_single) 16 (int (*)(...))Derive_single::~Derive_single 24 (int (*)(...))Derive_single::~Derive_single 32 (int (*)(...))Base_D::add 40 (int (*)(...))Derive_single::print 48 (int (*)(...))Derive_single::Derive_single_print Class Derive_single size=24 align=8 base size=20 base align=8 Derive_single (0x0x7fb8e93fe8f0) 0 vptr=((& Derive_single::_ZTV13Derive_single) + 16u) Base_D (0x0x7fb8e93fe958) 0 primary-for Derive_single (0x0x7fb8e93fe8f0) Base_C (0x0x7fb8e91857e0) 0 primary-for Base_D (0x0x7fb8e93fe958)
The memory layout is shown below and the memory layout is consistent with the description above:
C++ object model under multiple inheritance (non-rhombic)
Next, consider a non-rhombic multi-inheritance scenario, in which the derived class makes a "copy" of the virtual function table of each of its base classes, eventually forming thevirtual function table set (math.), the order in which the table of virtual functions is arranged is determined by the order in which the base class is declared in the class definition.
Virtual functions of derived classes are placed in the virtual function table of the first base class declared, when a derived class overrides a base class function, it overrides all corresponding functions in the base class.
// The implementation part of the class is omitted here class Base_A { public: Base_A(int i); virtual ~Base_A(); int getValue(); static void countA(); virtual void print(void); private: int baseA; static int baseAS; }; class Base_B { public: Base_B(int i); virtual ~Base_B(); int getValue(); virtual void add(void); static void countB(); virtual void print(void); private: int baseB; static int baseBS; }; class Derive_multiBase : public Base_A, public Base_B { public: Derive_multiBase(int d); void add(void) override; void print(void) override; virtual void Derive_multiBase_print(); private: int Derive_multiBaseValue; };
The print class memory layout is as follows:
Vtable for Derive_multiBase Derive_multiBase::_ZTV16Derive_multiBase: 13u entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTI16Derive_multiBase) 16 (int (*)(...))Derive_multiBase::~Derive_multiBase 24 (int (*)(...))Derive_multiBase::~Derive_multiBase 32 (int (*)(...))Derive_multiBase::print 40 (int (*)(...))Derive_multiBase::add 48 (int (*)(...))Derive_multiBase::Derive_multiBase_print 56 (int (*)(...))-16 64 (int (*)(...))(& _ZTI16Derive_multiBase) 72 (int (*)(...))Derive_multiBase::_ZThn16_N16Derive_multiBaseD1Ev 80 (int (*)(...))Derive_multiBase::_ZThn16_N16Derive_multiBaseD0Ev 88 (int (*)(...))Derive_multiBase::_ZThn16_N16Derive_multiBase3addEv 96 (int (*)(...))Derive_multiBase::_ZThn16_N16Derive_multiBase5printEv Class Derive_multiBase size=32 align=8 base size=32 base align=8 Derive_multiBase (0x0x7fb8e910cd20) 0 vptr=((& Derive_multiBase::_ZTV16Derive_multiBase) + 16u) Base_A (0x0x7fb8e91855a0) 0 primary-for Derive_multiBase (0x0x7fb8e910cd20) Base_B (0x0x7fb8e9185600) 16 vptr=((& Derive_multiBase::_ZTV16Derive_multiBase) + 72u)
From the memory layout, you can see that there are two vptr's (each pointing to two virtual function tables) corresponding to theDerive_multiBase
From two base classesBase_A
、Base_B
The table of virtual functions obtained by copying.
derived classDerive_multiBase
All virtual functions in thePrimary virtual table (PRIMARY VIRTUAL TABLE)That is to say, fromBase_A
The table of virtual functions obtained by copying.
through (a gap)Base_B
The table of virtual functions obtained by copying is also calledSecondary virtual tables (secondary virtual tables), from the memory layout of itsoffset
because of-16because this virtual function table pointer is 16 bytes from the initial location in the object's memory.
Also note that the dummy function symbol in this dummy function table isnon-virtual thunk to...
This has to do with the mechanism of function jumping, which is corrected by thunk for the addresses of functions that call different parent classes, seeExplore C++ Polymorphism ② - Inheritance Relationships、C++ object modelThe introduction in the
// thunk A segment of code associated (in this ABI) with a target function, which is called instead of the target function for the purpose of modifying parameters (. this) or other parts of the environment before transferring control to the target function, and possibly making further modifications after its return. A thunk may contain as little as an instruction to be executed prior to falling through to an immediately following target function, or it may be a full function with its own stack frame that does a full call to the target function.
The memory layout is shown below:
Discussion: how the enable_shared_from_this feature affects memory layout
enable_shared_from_thisThe documentation has the following description:
A common implementation for enable_shared_from_this is to hold a weak reference (such as std::weak_ptr) to *this. For the purpose of exposition, the weak reference is called weak-this and considered as a mutable std::weak_ptr member.
The usual implementation of enable_shared_from_this is to let the instance have a"Weak references."The following are some of the most important aspects of the program.Example has a member variable std::weak_ptr。
can be verified on test code for single-inheritance scenarios for theDerive_single
The class adds a class that inherits fromstd::enable_shared_from_this<Derive_single>
Otherwise unchanged:
class Derive_single : public Base_D, public std::enable_shared_from_this<Derive_single> { public: Derive_single(int d); void print(void) override; virtual void Derive_single_print(); private: int Derive_singleValue; };
Vtable for Derive_single Derive_single::_ZTV13Derive_single: 7u entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTI13Derive_single) 16 (int (*)(...))Derive_single::~Derive_single 24 (int (*)(...))Derive_single::~Derive_single 32 (int (*)(...))Base_D::add 40 (int (*)(...))Derive_single::print 48 (int (*)(...))Derive_single::Derive_single_print Class Derive_single size=40 align=8 base size=36 base align=8 Derive_single (0x0x7fd5c76431c0) 0 vptr=((& Derive_single::_ZTV13Derive_single) + 16u) Base_D (0x0x7fd5c7639750) 0 primary-for Derive_single (0x0x7fd5c76431c0) Base_C (0x0x7fd5c7632780) 0 primary-for Base_D (0x0x7fd5c7639750) std::enable_shared_from_this<Derive_single> (0x0x7fd5c76327e0) 16
Comparison of the preceding text reveals thatDerive_single
The memory footprint increases from 24 bytes to 40 bytes due to thestd::enable_shared_from_this<Derive_single>
The inheritance takes up an extra 16 bytes. Fromstd::weak_ptrIt is known from the documentation of thestd::weak_ptr
The typical implementation actually stores two pointers, consistent with the 16-byte memory growth here.
// std::weak_ptr Like std::shared_ptr, a typical implementation of weak_ptr stores two pointers: -- a pointer to the control block; -- the stored pointer of the shared_ptr it was constructed from.
Also, pay special attention to this timeDerive_single
There is no difference between the class virtual function table and the previous section, so theThe enable_shared_from_this feature does not affect the contents of the virtual function table。
bibliography
Itanium C++ ABI
C++ Object Model: Object Memory Layout Explained
C++ object model
C++ In-Depth Exploration of C++ Polymorphism ② - Inheritance Relationships
Virtual Function Inheritance - A Primer on Thunk Techniques
C++: Virtual Function Memory Layout Analysis (with clang compiler)