See stroustrup's answer (http://www.research.att.com /~ BS/bs_faq2.html # vcall)
Although calling a virtual member function in constructor is not a very common technique, studying it can deepen the understanding of the virtual function mechanism and object construction process. This problem is also different from the general intuitive understanding. Let's take a look at the two class definitions below.
Struct c180
{
C180 (){
Foo ();
This-> Foo ();
}
Virtual Foo (){
Cout <"<c180.foo this:" <This <"vtadr:" <* (void **) This <Endl;
}
};
Struct c190: Public c180
{
C190 (){}
Virtual Foo (){
Cout <"<c190.foo this:" <This <"vtadr:" <* (void **) This <Endl;
}
};
There is a virtual function in the parent class, and the parent class calls this virtual function in its constructor. When calling this function, it adopts two methods: direct call, one is called through the this pointer. At the same time, the subclass overrides this virtual function.
We can predict what will happen if a c190 object is constructed.
We know that when constructing an object, the process is as follows:
1) First, a piece of memory (on heap or on Stack) will be obtained based on the object size ),
2) use the pointer pointing to the memory as the this pointer to call the class constructor and initialize the memory.
3) if the object has a parent class, it will first call the constructor of the parent class (and recursion in turn ),If there are multiple parent classes (multiple inheritance), the constructor of the parent class will be called in sequence, and the position of this pointer will be adjusted as appropriate. After calling the constructor of all parent classes, execute your ownCode.
When c190 is constructed according to the above analysis, the c180 constructor will also be called. In this case, the first Foo call in the c180 constructor is static binding, and the c180: Foo () function will be called. The second Foo call is called through a pointer, And the polymorphism will occur. The c190: Foo () function should be called.
Run the following code:
C190 OBJ;
OBJ. Foo ();
Result:
<C180.foo this: 0012f7a4 vtadr: 0045c404
<C180.foo this: 0012f7a4 vtadr: 0045c404
<C190.foo this: 0012f7a4 vtadr: 0045c400
This is quite different from our analysis. The first two rows are the output when c190 is constructed, and the last row is the c190: Foo () function called by static binding. Output from row 3 shows that the polymorphism does not happen as expected. In addition, when comparing the last output column, it is found that the virtual table corresponding to the object and the virtual table corresponding to the constructed object are not the same when the c180 constructor is called. In fact, this is where the mysteries are.
For this reason, I checked the C ++ Standard Specification. There are clear provisions in Article 12.7.3. This is a special case. In this case, the constructor of the parent class is called when the child class is constructed, and the constructor of the parent class calls the virtual member function, this virtual member function does not allow polymorphism even if it is overwritten by the quilt class. That is, the virtual function of the parent class must be called without rewriting the Child class.
I want to do this because when calling the constructor of the parent class, the member variables in the object that belong to the subclass must have not been initialized, because the code in the subclass constructor has not been executed. If polymorphism is allowed at this time, that is, the virtual function of the subclass is called through the constructor of the parent class, and this virtual function may fail to access data members of the subclass.
Let's take a look at the compiled code generated by vc7.1 to easily understand this behavior.
This is the c190 constructor:
01 000000fe0 push EBP
02 000000fe1 mov EBP, esp
03 000000fe3 sub ESP, 0cch
04 00426fe9 push EBX
05 00426fea push ESI
06 00426feb push EDI
07 00426fec push ECx
08 00426fed Lea EDI, [EBP + ffffff34h]
09 000000ff3 mov ECx, 33 H
10 000000ff8 mov eax, 0 cccccccch
11 000000ffd rep STOs dword ptr [EDI]
12 000000fff pop ECx
13 00427000 mov dword ptr [ebp-8], ECX
14 00427003 mov ECx, dword ptr [ebp-8]
15 00427006 call 0041d451
16 0042700b mov eax, dword ptr [ebp-8]
17 0042700e mov dword ptr [eax], 45c400h
18 00427014 mov eax, dword ptr [ebp-8]
19 00427017 pop EDI
20 00427018 pop ESI
21 00427019 pop EBX
22 0042701a add ESP, 0cch
23 00427020 cmp ebp, esp
24 00427022 call 0041ddf2
25 00427027 mov ESP, EBP
26 00427029 pop EBP
27 0042702a RET
the commands in the beginning are explained in the previous articles. Let's see that 15th is a call to the constructor c180: c180 () of the parent class. According to the preceding instructions, we know that the this pointer is put in ECx, that is, the address of the c190 object. If you jump to the batch address of this pointer, you will find that the value 0 x cccccccc is not initialized, and the virtual table pointer is not initialized. So let's jump to the c180 constructor.
01 00427040 push EBP
02 00427041 mov EBP, esp
03 00427043 sub ESP, 0cch
04 00427049 push EBX
05 0042704a push ESI
06 0042704b push EDI
07 0042704c push ECx
08 0042704d Lea EDI, [EBP + ffffff34h]
09 00427053 mov ECx, 33 H
10 00427058 mov eax, 0 cccccccch
11 0042705d rep STOs dword ptr [EDI]
12 0042705f pop ECx
13 00427060 mov dword ptr [ebp-8], ECX
14 00427063 mov eax, dword ptr [ebp-8]
15 00427066 mov dword ptr [eax], 45c404h
16 0042706c mov ECx, dword ptr [ebp-8]
17 0042706f call 0041da8c
18 00427074 mov ECx, dword ptr [ebp-8]
19 00427077 call 0041da8c
20 0042707c mov eax, dword ptr [ebp-8]
21 0042707f pop EDI
22 00427080 pop ESI
23 00427081 pop EBX
24 00427082 add ESP, 0cch
25 00427088 cmp ebp, esp
26 0042708a call 0041ddf2
27 0042708f mov ESP, EBP
28 00427091 pop EBP
29 00427092 RET
Let's take a look at row 15th, and fill in a 4-byte value 0x0045c404 at the position of the this pointer, that is, the virtual table address of c180 that we printed earlier. Rows 16th, 17, 18, and 19 call the Foo () function twice, respectively, and use static binding. This is a bit strange, because we use the this pointer for the next call, it should be dynamic binding. But here is static binding. Why is the compiler doing this optimization? Let's look at it later.
After this function is executed, we will return to the c190 constructor. Let's look at the 17th lines of the c190 constructor assembly code. Here we reenter 0x0045c400 at the beginning of the object, overwrite the original value, and this value is the real c190 virtual table address we printed earlier.
That is to say, vc7.1 sets the virtual pointer value of the object to the virtual table of the corresponding class before calling the real code of the constructor to implement the corresponding semantics of the C ++ specification. The C ++ standard only specifies the behavior and does not specify the method used by the compiler to implement this line. As we can see above, even if it is called through the this pointer, the compiler will optimize it to static binding, that is, it will not be wrong even if this virtual pointer is not adjusted. The reason why I want to adjust it is probably to prevent other virtual functions from being called through the this pointer in the called virtual member. But who will be so abnormal?
It is also worth mentioning that vc7.1 has an extension attribute that can be used to suppress the code generated by the compiler to adjust the virtual pointer. We can add this attribute to the c180 class declaration.
Struct _ declspec (novtable) c180
{
C180 (){
Foo ();
This-> Foo ();
}
Virtual Foo (){
Cout <"<c180.foo this:" <This <"vtadr:" <* (void **) This <Endl;
}
};
Then execute the preceding code and the output will become:
<C180.foo this: 0012f7a4 vtadr: cccccccc
<C180.foo this: 0012f7a4 vtadr: cccccccc
<C190.foo this: 0012f7a4 vtadr: 0045c400
Because the compiler suppresses the adjustment of the virtual pointer, the value of the virtual pointer is not initialized when the c180 constructor is called, then we can see that thanks to the compiler, the second call to foo through the this pointer is optimized to static binding. Otherwise, a pointer Exception error will occur because the virtual pointer is not initialized, in this case, we will answer the above question.
In this case, I will not list the compilation code generated. If you are interested, you can take a look at it yourself. In addition, if you are interested in calling the destructor, analyze it yourself.
In addition, this attribute is widely used in ATL code. In ATL, the interface is generally a pure virtual base class. If this optimization attribute is not used, the constructor of the parent class must be called in the subclass that implements the constructor of the class, the parent class constructor generated by the compiler must set the value of the virtual pointer. Therefore, the compiler must construct the virtual table of the parent class. In fact, this virtual table has no significance, because the virtual functions of the pure Virtual Interface Class of ATL are not implemented. In this way, it is not only a few additional useless set-value commands, but also a waste of space. If you are interested, you can verify it by yourself.