【问题标题】:Is an object allowed to legally change its type during its lifetime in C++?是否允许对象在 C++ 的生命周期内合法地更改其类型?
【发布时间】:2012-09-10 10:54:03
【问题描述】:

我有这个代码:

class Class {
public:
    virtual void first() {};
    virtual void second() {};
};

Class* object = new Class();
object->first();
object->second();
delete object;

我使用 Visual C++ 10 和 /O2 编译并进行了反汇编:

282:    Class* object = new Class();
00403953  push        4  
00403955  call        dword ptr [__imp_operator new (4050BCh)]  
0040395B  add         esp,4  
0040395E  test        eax,eax  
00403960  je          wmain+1Ch (40396Ch)  
00403962  mov         dword ptr [eax],offset Class::`vftable' (4056A4h)  
00403968  mov         esi,eax  
0040396A  jmp         wmain+1Eh (40396Eh)  
0040396C  xor         esi,esi  
283:    object->first();
0040396E  mov         eax,dword ptr [esi]  
00403970  mov         edx,dword ptr [eax]  
00403972  mov         ecx,esi  
00403974  call        edx  
284:    object->second();
00403976  mov         eax,dword ptr [esi]  
00403978  mov         edx,dword ptr [eax+4]  
0040397B  mov         ecx,esi  
0040397D  call        edx  
285:    delete object;
0040397F  push        esi  
00403980  call        dword ptr [__imp_operator delete (405138h)]  

请注意,在00403968 处,对象起始地址(存储vptr 的位置)被复制到esi 寄存器中。然后在0040396E,此地址用于检索vptrvptr 值用于检索first() 的地址。然后在00403976再次检索vptr,用于检索second()的地址。

为什么 vptr 被检索了两次?该对象是否可能在两次调用之间更改其vptr,还是只是优化不足?

【问题讨论】:

  • 很奇怪。我认为这一切都应该是无操作的。这可能是优化不足,如果它查看函数内部可能会发现它们都没有改变esi

标签: c++ visual-c++ polymorphism rtti virtual-functions


【解决方案1】:

为什么 vptr 被检索了两次?对象是否可能在两次调用之间更改其 vptr,还是只是优化不足?

考虑:

object->first();

此调用可能会破坏对象并在同一块内存中创建一个新对象。因此,在此调用之后,无法对状态做出任何假设。例如:

#include <new>

struct Class {
    virtual void first();
    virtual void second() {}
    virtual ~Class() {}
};

struct OtherClass : Class {
    void first() {}
    void second() {}
};

void Class::first() {
    void* p = this;
    static_assert(sizeof(Class) == sizeof(OtherClass), "Oops");
    this->~Class();
    new (p) OtherClass;
}

int main() {
    Class* object = new Class();
    object->first();
    object->second();
    delete object;
}

如果该函数是内联的和/或使用链接时代码生成,编译器可能会优化掉不必要的寄存器加载。


正如 DeadMG 和 Steve Jessop 所指出的,上述代码表现出未定义的行为。根据 C++ 2003 标准的 3.8/7:

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:

  • 新对象的存储空间正好覆盖了原始对象占用的存储位置,并且
  • 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且
  • 原始对象的类型不是 const 限定的,如果是类类型,则不包含任何类型为 const 限定或引用类型的非静态数据成员,并且
  • 原始对象是 T 类型的最衍生对象 (1.8),而新对象是 T 类型的最衍生对象(也就是说,它们不是基类子对象)。

上面的代码不满足上面列表中的要求 2。

【讨论】:

  • @LuchianGrigore:也许 LTCG没有帮助,但它是一种优化,可供潜在的未来编译器使用。
  • @LuchianGrigore:gcc-4.7.1 将原始代码优化为发布到operator new() 的调用中,然后使用-O3 调用operator delete()。您是否愿意为您的主张提供任何支持证据?
  • @MaximYegorushkin 你用 gcc 和 Visual C++ 吗?因为否则,你没有回答这个问题。这是关于具有特定设置 (-O2) 的特定编译器。而对于 Visual C++,-O2 与链接时代码生成产生相同的结果。
  • 并非如此。在那个空间中创建除另一个 Class 之外的任何东西都将是一大堆 UB。编译器不必担心这种潜在的情况。
  • @DeadMG:在那个空间中创建除了另一个类之外的任何东西都将是一大堆 UB。 嗯,不是真的。您愿意为这种大胆的主张提供任何支持证据吗?
【解决方案2】:

先回答题目中的问题:

是的,派生类的对象在构造和销毁期间会更改其类型。这是唯一的情况。

问题正文中的代码不同。但正如 Maxim 正确指出的那样,您只有一个指针。此指针可能(在不同时间)指向位于同一地址的两个不同对象。

【讨论】:

    【解决方案3】:

    它存储在esi 中,以便在不同函数调用之间保存。

    微软惯例说

    如果函数中使用了 ESI、EDI、EBX 和 EBP 寄存器,编译器会生成 prolog 和 epilog 代码以保存和恢复它们。

    因此存储在esi 中的指针将保留,但ecx 中的this 指针可能不会。

    【讨论】:

    • 好的,对象的开头存储在esi中,但是为什么vptr被读取了两次?
    • 同时保留vptr 需要一个额外的寄存器,例如edi,然后必须保存和恢复。那会改进代码吗?很难说。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-07
    • 2014-03-15
    • 1970-01-01
    • 1970-01-01
    • 2013-06-11
    相关资源
    最近更新 更多