【问题标题】:Does the vptr change during destruction?vptr 在销毁过程中会发生变化吗?
【发布时间】:2011-10-27 13:48:29
【问题描述】:

我在看this article,它说“在进入基类析构函数时,对象变成了基类对象,C++ 的所有部分——虚函数、dynamic_casts 等——都以这种方式对待它。 "这是否意味着 vptr 在销毁过程中发生了变化?这是怎么发生的?

【问题讨论】:

  • 这都是非常具体的实现。
  • @larsmans:这很少是特定于实现的,除非您正在考虑不使用虚拟表的实现,其中没有虚拟表。在所有使用虚函数的实现中,vptr 必须在销毁期间更改。
  • @Larsmans,我认为可以安全地假设任何提到 vtables 和 vptrs 的问题都以限定词“在使用 vtables 和 vptrs 的实现中,...”开头。这样我们就不必一直看到迂腐的 cmets 指出 vtables 和 vptrs 是标准不需要的实现细节。

标签: c++ destruction vptr


【解决方案1】:

在所有使用虚函数表的实现(即所有当前的 C++ 实现)中,答案是肯定的,vptr 更改为正在执行的析构函数的类型。原因是标准要求被析构对象的类型被执行的析构函数的类型。

如果你有 B、D、MD 三种类型(基础、派生、最派生)的层次结构,并且你实例化并销毁了 MD 类型的对象,那么在执行 MD::~MD() 时,对象的类型 MD,但是当隐式调用基析构函数时,对象的运行时类型必须是D。这是通过更新vptr 来实现的。

【讨论】:

  • 类型必须是正确的原因是它是可观察的,通过在销毁期间调用虚函数。必须调用正确的覆盖,这可能与在销毁开始之前调用的覆盖不同,这意味着 vptr 必须已更新。
  • @JonathanWakely: 正确,如果在销毁期间不更新 vptr,对虚函数的调用最终可能会被分派给析构函数已经完成的类型的覆盖器(这是其他语言中的行为,例如 Java)。
  • 这是否也适用于构造函数?
  • @hl3mukkel:是的,除了方向相反(从基础到大多数派生)
【解决方案2】:

学究式的 C++ 答案当然是“标准没有说明 vtbls 或多态是如何实现的。”

但是,实际上,是的。 vtbl 在基类的析构函数开始执行之前被修改。

编辑:

以下是我如何使用 MSVC10 亲眼目睹这一切。一、测试代码:

#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();
}

现在,在 Poly dtor 的左大括号处设置一个断点。

当您运行此代码时,它在左大括号处中断(就在构造函数的主体开始执行之前),您可以查看 vptr:

另外,您可以查看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  

越过下一行,进入析构函数的主体,再看一眼 vptr:

现在,当我们从析构函数体中调用DoTest 时,vtbl 已经被修改为指向purecall_,这会在调试器中生成运行时断言错误:

【讨论】:

  • 我不认为测试是决定性的,如果是这样,它会指向完全奇怪的地方,其中 vtable 机制将在析构函数中分派给一个指针从未在 vtable 中的函数 (在破坏开始之前)。这个特定测试的问题是您直接从析构函数调用虚拟指针,此时因为编译器知道对象的确切类型 (Poly),所以它没有使用动态调度,而是使用静态调度。要修复测试,您可以创建一个接受 Poly&amp; 并在其上调用 Test 的函数,强制调度
  • 由于 vtable 是特定于实现的,也许只是在析构函数中查看 *reinterpret_cast(this) ? :)
猜你喜欢
  • 1970-01-01
  • 2011-06-25
  • 1970-01-01
  • 2023-03-23
  • 2013-08-20
  • 1970-01-01
  • 1970-01-01
  • 2021-10-23
  • 2021-11-09
相关资源
最近更新 更多