10

I was looking at this article, and it says "Upon entry to the base class destructor, the object becomes a base class object, and all parts of C++—virtual functions, dynamic_casts, etc.—treat it that way." Does this mean that the vptr has changed during destruction? How does that happen?

pythonic metaphor
  • 9,476
  • 16
  • 59
  • 103
  • 2
    This is all very implementation-specific. – Fred Foo Oct 27 '11 at 13:51
  • 5
    @larsmans: That is rarely implementation-specific, unless you are considering implementations that do not use virtual tables, of which there are none. In all implementations that use virtual functions, the `vptr` must be changed during destruction. – David Rodríguez - dribeas Oct 27 '11 at 13:56
  • 4
    @Larsmans, I think it's safe to pretend that any question that mentions vtables and vptrs is prefaced with the qualifier "In implementations that use vtables and vptrs, ... ." Then we don't have to keep seeing pedantic comments pointing out that vtables and vptrs are an implementation detail not required by the standard. – Rob Kennedy Oct 27 '11 at 14:06

2 Answers2

12

In all implementations that use virtual function tables (i.e. all current C++ implementations) the answer is yes, the vptr changes to that of the type of the destructor that is being executed. The reason is that the standard requires that the type of the object being destructed is the type of the destructor being exectued.

If you have a hierarchy of three types B, D, MD (base, derived, most derived) and you instantiate and destroy an object of type MD, then while executing MD::~MD() the type of the object is MD, but when the implicit call to the base destructor is executed, the runtime type of the object must be D. This is achieved by updating the vptr.

David Rodríguez - dribeas
  • 192,922
  • 20
  • 275
  • 473
  • 3
    The reason the type _must be_ right is that it's observable, by calling a virtual function during destruction. The right override must be called, and that might be different to the override that would have been called before destruction started, which means the vptr must have been updated. – Jonathan Wakely Dec 17 '12 at 14:46
  • 1
    @JonathanWakely: Correct, without the *vptr* being updated during destruction a call to a virtual function could end up being dispatched to an overrider in a type whose destructor has already completed (This is the behavior in other languages, like Java). – David Rodríguez - dribeas Dec 17 '12 at 18:28
  • Does this apply to the constructor aswell? – hl3mukkel Oct 20 '17 at 23:46
  • 2
    @hl3mukkel: Yes, except that in the opposite direction (from base to most derived) – David Rodríguez - dribeas Oct 22 '17 at 22:04
6

The pedantic C++ answer is, of course, "The Standard doesn't say anything about vtbls or how polymorphism is implemented."

However, practically speaking, yes. The vtbl is modified before the body of the base class' destructor begins execution.

EDIT:

Here is how I used MSVC10 to see this happen for myself. First, the test code:

#include <string>
#include <iostream>
using namespace std;

class Poly
{
public:
    virtual ~Poly(); 
    virtual void Foo() const = 0;
    virtual void Test() const = 0 { cout << "PolyTest\n"; }
};

class Left : public Poly
{
public:
    ~Left() 
    { 
        cout << "~Left\n"; 
    }
    virtual void Foo() const {  cout << "Left\n"; }
    virtual void Test() const  { cout << "LeftTest\n"; }
};

class Right : public Poly
{
public:
    ~Right() { cout << "~Right\n"; }
    virtual void Foo() const { cout << "Right\n"; }
    virtual void Test() const { cout << "RightTest\n"; }
};

void DoTest(const Poly& poly)
{
    poly.Test();
}

Poly::~Poly() 
{  // <=== BKPT HERE
    DoTest(*this);
    cout << "~Poly\n"; 
}

void DoIt()
{
    Poly* poly = new Left;
    cout << "Constructed...\n";
    poly->Test();
    delete poly;
    cout << "Destroyed...\n";
}

int main()
{
    DoIt();
}

Now, set a breakpoint at the opening brace for the Poly dtor.

When you run this code at it breaks on the opening brace (just before the body of the constructor begins executing), you can take a peek at the vptr:enter image description here

Also, you can view the disassembly for the Poly dtor:

Poly::~Poly() 
{ 
000000013FE33CF0  mov         qword ptr [rsp+8],rcx  
000000013FE33CF5  push        rdi  
000000013FE33CF6  sub         rsp,20h  
000000013FE33CFA  mov         rdi,rsp  
000000013FE33CFD  mov         ecx,8  
000000013FE33D02  mov         eax,0CCCCCCCCh  
000000013FE33D07  rep stos    dword ptr [rdi]  
000000013FE33D09  mov         rcx,qword ptr [rsp+30h]  
000000013FE33D0E  mov         rax,qword ptr [this]  
000000013FE33D13  lea         rcx,[Poly::`vftable' (13FE378B0h)]  
000000013FE33D1A  mov         qword ptr [rax],rcx  
    DoTest(*this);
000000013FE33D1D  mov         rcx,qword ptr [this]  
000000013FE33D22  call        DoTest (13FE31073h)  
    cout << "~Poly\n"; 
000000013FE33D27  lea         rdx,[std::_Iosb<int>::end+4 (13FE37888h)]  
000000013FE33D2E  mov         rcx,qword ptr [__imp_std::cout (13FE3C590h)]  
000000013FE33D35  call        std::operator<<<std::char_traits<char> > (13FE3104Bh)  
}
000000013FE33D3A  add         rsp,20h  
000000013FE33D3E  pop         rdi  
000000013FE33D3F  ret  

Step over the next line, in to the body of the destructor, and take another peek at the vptr:

enter image description here Now when we call DoTest from within the body of the destructor, the vtbl has already been modified to point to purecall_, which generates a runtime assertion error in the debugger:

John Dibling
  • 94,084
  • 27
  • 171
  • 303
  • 3
    I don't think that the test is conclusive, and if it were it would point to utter weirdness, where the vtable mechanism would dispatch in the destructor to a function whose pointer was never in the vtable (before destruction started). The problem with this particular test is that you are calling the virtual pointer directly from the destructor, and at that point because the compiler knows the exact type of the object (`Poly`) it is not using dynamic dispatch, but rather static dispatch. To fix the test, you can create a function that takes `Poly&` and calls `Test` on that, forcing the dispatch – David Rodríguez - dribeas Oct 27 '11 at 14:20
  • As vtable is implementation specific, maybe simply to look at *reinterpret_cast(this) in destructors? :) – user396672 Oct 27 '11 at 14:40