C++ virtual inheritance memory object model

Article directory

  • C++ virtual inheritance memory object model
    • 1. Ordinary virtual inheritance
      • 1.1 Memory size
      • 1.2 Memory layout
      • 1.3 Pointer assignment
      • 1.4 Base class pointer call
      • 1.5 Calling subclass pointers
    • 2. Subclasses add their own virtual functions
      • 2.1 Virtual function table
      • 2.2 Virtual inheritance table
      • 2.3 Memory layout
    • 3. Diamond inheritance
    • 4. Summary

C++ virtual inheritance memory object model

Compared with other development languages, C++ introduces a more complex concept, that is, multiple inheritance, which brings great complexity to the memory layout of C++. For ordinary multiple inheritance, you can refer to the article C++ + Class virtual function memory object model.

One situation that ordinary multiple inheritance cannot handle is diamond inheritance, such as the implementation of the iostream class:

If you follow ordinary inheritance, then there will be two ios data in the iostream memory. This operation and use will bring great disadvantages. In order to solve this problem, C + + Virtual inheritance is introduced. This article will discuss the memory layout principle of virtual inheritance.

1. Ordinary virtual inheritance

First let’s look at the common situation:

namespace NameIso
{<!-- -->
class ios
{<!-- -->
public:
ios() {<!-- -->}
virtual ~ios() {<!-- -->}
virtual void Fun()
{<!-- -->
std::cout << "FunIos" << std::endl;
}
private:
int m_iosData = 10;
};

class istream : virtual public ios
{<!-- -->
public:
istream() {<!-- -->}
virtual ~istream() {<!-- -->}
void Fun() override
{<!-- -->
std::cout << "FunIstream" << std::endl;
}
private:
int m_istreamData = 100;
};

    voidVirtual()
{<!-- -->
istream* p = new istream;
ios* i = p;
std::cout << sizeof(*p) << std::endl;
i->Fun();
p->Fun();
delete p;
}
}

Here we look at a few questions:

  1. The memory size of istream.
  2. The memory layout of istream.
  3. ios* i = p; Pointer assignment process.
  4. i->Fun();The process of calling a function using a base class.
  5. p->Fun()The process of using a subclass to call a virtual function.

1.1 Memory size

Let’s look at the memory size first:

20

The returned memory size is 20, and there are only two data programs (8 itself). It can be guessed that in order to support virtual inheritance, C++ has made relatively large changes to the object memory.

1.2 Memory layout

We use the debugger to look at the memory layout information of this object:

0:000> dt p
Local var @ 0x135fd80 Type NameIso::istream*
0x015dac98
    + 0x00c __VFN_table : 0x007e8b94
    + 0x010 m_iosData : 0n10
    + 0x004 m_istreamData : 0n100

I found a problem here. The memory layout is messy and there are no rules for offsets. Here we use another command to view the original memory.

0:000> dds 0x015dac98 L8
015dac98 007e8ba0 CPPDemo!NameIso::istream::`vbtable'
015dac9c 00000064
015daca0 00000000
015daca4 007e8b94 CPPDemo!NameIso::istream::`vftable'
015daca8 0000000a
015dacac fdfdfdfd
015dacb0 abababab
015dacb4 abababab

From this you can see:

  1. The header of istream is a pointer to vbtable
  2. Then put your own member 00000064.
  3. Then a 00000000.
  4. Then store the ios information.

The structure information for vbtable is as follows:

0:000> dd 007e8ba0 L2
007e8ba0 00000000 0000000c

Here I will roughly explain what it means:

  1. vbtable[0] = 00000000: Don’t worry about what it means for now (we will talk about it later).
  2. vbtable[1] = 0000000c : The memory offset length of the virtual inheritance base object.

So here we roughly draw the memory layout information of istream:

1.3 Pointer assignment

Let’s take a look at how ios* i = p; operates on this assignment:

 ios* i = p;
006E2CD2 cmp dword ptr [p],0
006E2CD6 jne NameIso::Virtual + 51h (06E2CE1h)
006E2CD8 mov dword ptr [ebp-58h],0
006E2CDF jmp NameIso::Virtual + 5Fh (06E2CEFh)
006E2CE1 mov eax,dword ptr [p] //Get the address of vbtable
006E2CE4 mov ecx,dword ptr [eax] //Get vbtable
006E2CE6 mov edx,dword ptr [p] //istream address
006E2CE9 add edx,dword ptr [ecx + 4] //Offset in vbtable + istream address
006E2CEC mov dword ptr [ebp-58h],edx //ios address
006E2CEF mov eax,dword ptr [ebp-58h] //Get the address of ios
006E2CF2 mov dword ptr [i],eax //Put the address into i.

From the above code we can conclude that the process is:

  1. Get vbtable offset
  2. Calculate ios address.
  3. The address is put into variable i.

It can be seen that the whole process is still very complicated.

1.4 Base class pointer call

The base class pointer is relatively simple and is consistent with ordinary virtual function calls, as follows:

 i->Fun();
006E2D10 mov eax,dword ptr [i] //virtual pointer
006E2D13 mov edx,dword ptr [eax] //virtual table
006E2D15 mov ecx,dword ptr [i]
006E2D18 mov eax,dword ptr [edx + 4] //Function address
006E2D1B call eax

1.5 Calling subclass pointers

The call of the subclass pointer is more complicated. You need to find the virtual function of the base class first, and then call:

 p->Fun();
006E2D1D mov eax,dword ptr [p]
006E2D20 mov ecx,dword ptr [eax]
006E2D22 mov edx,dword ptr [ecx + 4] //Virtual tables are cheap
006E2D25 mov eax,dword ptr [p]
006E2D28 mov ecx,dword ptr [eax]
006E2D2A mov eax,dword ptr [p]
006E2D2D add eax,dword ptr [ecx + 4]
006E2D30 mov ecx,dword ptr [p]
006E2D33 mov edx,dword ptr [ecx + edx] //Base class virtual table
006E2D36 mov ecx,eax
006E2D38 mov eax,dword ptr [edx + 4] //Address of virtual function
006E2D3B call eax

2. Subclasses add their own virtual functions

This time we add a virtual function to the subclass, as follows:

namespace NameIso
{<!-- -->
class ios
{<!-- -->
public:
ios() {<!-- -->}
virtual ~ios() {<!-- -->}
virtual void Fun()
{<!-- -->
std::cout << "FunIos" << std::endl;
}
private:
int m_iosData = 10;
};

class istream : virtual public ios
{<!-- -->
public:
istream() {<!-- -->}
virtual ~istream() {<!-- -->}
void Fun() override
{<!-- -->
std::cout << "FunIstream" << std::endl;
}
virtual void MyFun() //New virtual function
{<!-- -->
std::cout << "MyFunIstream" << std::endl;
}
private:
int m_istreamData = 100;
};
}

We can first ask a question, where should MyFun be placed (if it is not virtual inheritance, the C++ class virtual function memory object model indicates that this address is placed in the virtual function table of the base class) , so what difference does the introduction of virtual inheritance make?

0:000> dds poi(p) L8
00cc5958 00068b94 CPPDemo!NameIso::istream::`vftable'
00cc595c 00068ba8 CPPDemo!NameIso::istream::`vbtable'
00cc5960 00000064
00cc5964 00000000
00cc5968 00068b9c CPPDemo!NameIso::istream::`vftable'
00cc596c 0000000a
00cc5970 fdfdfdfd
00cc5974 abababab

There is an extra virtual function table CPPDemo!NameIso::istream::`vftable’. At this time we take a look at two data members:

  1. CPPDemo!NameIso::istream::vftable
  2. CPPDemo!NameIso::istream::vbtable

Let’s see what the difference is between these two.

2.1 Virtual function table

Here we see that this virtual function table actually only contains one function MyFun.

0:000> dds 00068b94
00068b94 0006152d CPPDemo!ILT + 1320(?MyFunistreamNameIsoUAEXXZ)
00068b98 00069070 CPPDemo!NameIso::istream::`RTTI Complete Object Locator'

2.2 Virtual inheritance table

Let’s take a look at the address memory information of vbtable:

0:000> dd 00068ba8 L2
00068ba8 ffffffffc 0000000c

0:000> .formats ffffffffc
Evaluate expression:
  Hex: ffffffffc
  Decimal: -4
  Octal: 37777777774
  Binary: 11111111 11111111 11111111 11111100
  Chars: ....
  Time: ***** Invalid
  Float: low -1.#QNAN high 0
  Double: 2.122e-314

Here we see vbtable[0] = ffffffffc. In fact, vbtable[0] is the offset from vbtable to the starting address of istream (some people explain that it is the vftable offset to istream, but in fact the two are the same).

2.3 Memory layout

This memory layout information is as follows:

3. Diamond inheritance

Now it’s time to look at the diamond inherited memory structure:

namespace NameIso
{<!-- -->
class ios
{<!-- -->
public:
ios() {<!-- -->}
virtual ~ios() {<!-- -->}
virtual void Fun()
{<!-- -->
std::cout << "FunIos" << std::endl;
}
private:
int m_iosData = 10;
};

class istream : virtual public ios
{<!-- -->
public:
istream() {<!-- -->}
virtual ~istream() {<!-- -->}
void Fun() override
{<!-- -->
std::cout << "FunIstream" << std::endl;
}
virtual void MyFunIstream()
{<!-- -->
std::cout << "MyFunIstream" << std::endl;
}
private:
int m_istreamData = 100;
};

class ostream : virtual public ios
{<!-- -->
public:
ostream() {<!-- -->}
virtual ~ostream() {<!-- -->}
void Fun() override
{<!-- -->
std::cout << "FunOstream" << std::endl;
}
virtual void MyFunOstream()
{<!-- -->
std::cout << "MyFunOstream" << std::endl;
}
private:
int m_ostreamData = 110;
};

class iostream : public istream, public ostream
{<!-- -->
public:
iostream() {<!-- -->}
virtual ~iostream() {<!-- -->}
void Fun() override
{<!-- -->
std::cout << "FunIOstream" << std::endl;
}
void MyFunIstream() override
{<!-- -->
std::cout << "iostream MyFunIstream" << std::endl;
}
void MyFunOstream() override
{<!-- -->
std::cout << "iostream MyFunOstream" << std::endl;
}
private:
int m_iostreamData = 300;
};
}

The memory structure relationship at this time is as follows:

0x00bfac98
    + 0x020 __VFN_table : 0x00258c10
    + 0x024 m_iosData : 0n10
    + 0x000 __VFN_table : 0x00258c00
    + 0x008 m_istreamData : 0n100
    + 0x020 __VFN_table : 0x00258c10
    + 0x024 m_iosData : 0n10
    + 0x00c __VFN_table : 0x00258c08
    + 0x014 m_ostreamData : 0n110
    + 0x020 __VFN_table : 0x00258c10
    + 0x024 m_iosData : 0n10
    + 0x018 m_iostreamData : 0n300

0:000> dds poi(p) L10
00bfac98 00258c00 CPPDemo!NameIso::iostream::`vftable'
00bfac9c 00258c18 CPPDemo!NameIso::iostream::`vbtable'
00bfaca0 00000064
00bfaca4 00258c08 CPPDemo!NameIso::iostream::`vftable'
00bfaca8 00258c20 CPPDemo!NameIso::iostream::`vbtable'
00bfacac 0000006e
00bfacb0 0000012c
00bfacb4 00000000
00bfacb8 00258c10 CPPDemo!NameIso::iostream::`vftable'
00bfacbc 0000000a

As you can see from here, the inheritance of istream and ostream is completely a normal inheritance scenario. Here we directly give the memory layout:

As can be seen from this figure, the introduction of virtual inheritance makes the memory layout very complicated, and the efficiency is relatively slow.

4. Summary

The main purpose of virtual inheritance is to solve the problem of subclasses inheriting a base class multiple times. C++ solves this problem by introducing a virtual inheritance table. What is stored in the virtual inheritance table is the offset of a base class instance. No matter how many times it is inherited, there is only one base class instance.

Although virtual inheritance solves the problem of subclasses inheriting a base class multiple times, it has a relatively large impact on the memory layout and will also cause a loss in the calling performance of base class virtual functions.

We should avoid similar designs when designing, because this design brings two disadvantages:

  1. Efficiency loss (this C++ is relatively high-performance and can be ignored).
  2. Debugging is complex.