通过命名成员调用虚拟与地址或引用的区别

Difference calling virtual through named member versus address or reference

更新如下:在 clang 中,通过其名称使用多态对象的左值不会激活虚拟分派,但会通过其地址激活。

对于下面的基classB和派生D,虚函数something,联合Space

#include <iostream>
using namespace std;

struct B {
    void *address() { return this; }
    virtual ~B() { cout << "~B at " << address() << endl; }
    virtual void something() { cout << "B::something"; }
};

struct D: B {
    ~D() { cout << "~D at " << address() << endl; }
     void something() override { cout << "D::something"; }
};

union Space {
    B b;
    Space(): b() {}
    ~Space() { b.~B(); }
};

如果您的值 sSpace,在 Clang++ 中:(更新:错误地声称 g++ 具有相同的行为) 如果您执行 s.b.something(),将调用 B::something(),而不是在 s.b 上执行 动态绑定 ,但是,如果您调用 (&s.b)->something()将动态绑定到 b 真正包含的内容(BD)。 完成代码是这样的:

union SpaceV2 {
    B b;
    SpaceV2(): b() {}
    ~SpaceV2() { (&b)->~B(); }
};

static_assert(sizeof(D) == sizeof(B), "");
static_assert(alignof(D) == alignof(B), "");

#include <new>

int main(int argc, const char *argv[]) {
    {
        Space s;
        cout << "Destroying the old B: ";
        s.b.~B();
        new(&s.b) D;
        cout << "\"D::something\" expected, but \"";
        s.b.something();
        cout << "\" happened\n";
        auto &br = s.b;
        cout << "\"D::something\" expected, and \"";
        br.something();
        cout << "\" happened\n";
        cout << "Destruction of D expected:\n";
    }
    cout << "But did not happen!\n";
    SpaceV2 sv2;
    new(&sv2.b) D;
    cout << "Destruction of D expected again:\n";
    return 0;    
}

当使用 -O2 优化和我 运行 程序编译时,这是输出:

$./a.out 
Destroying the old B: ~B at 0x7fff4f890628
"D::something" expected, but "B::something" happened
"D::something" expected, and "D::something" happened
Destruction of D expected:
~B at 0x7fff4f890628
But did not happen!
Destruction of D expected again:
~D at 0x7fff4f890608
~B at 0x7fff4f890608

令我惊讶的是,使用 placement new 设置 s.b 的动态类型会导致通过其名称或地址在完全相同的左值上调用 something 的差异。第一个问题很重要,但我一直没能找到答案:

  1. 正在对派生的 class 进行 placement new,如 new(&s.b) D undefined behavior 根据 C++标准?
  2. 如果不是未定义的行为,这种不通过命名成员的左值激活虚拟分派的选择是在标准中指定的东西还是在 G++、Clang 中的选择?

谢谢,我在 S.O 中的第一个问题。曾经。

更新 参考标准的答案和评论是准确的:根据标准,s.b 将永远引用确切类型的对象 Bmemory允许更改类型,但是通过 s.b 对该内存的任何使用都是 "undefined behavior",即被禁止,或者编译器可以随心所欲地进行翻译。如果 Space 只是一个字符缓冲区,就地构造、析构、更改类型都是有效的。在导致这个问题的代码中正是这样做的,它适用于符合标准的 AFAIK。 谢谢

表达式 new(&s.b) D; 重新使用名为 s.b 且之前由 B 占用的存储来存储新的 D.

然而你写 s.b.something(); 。这会导致未定义的行为,因为 s.b 表示 B,但存储在该位置的实际对象是 D。参见 C++14 [basic.life]/7:

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and

  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and

    [...]

不满足最后一个要点,因为新类型不同。

(代码后面还有其他潜在问题,但由于此处导致了未定义的行为,因此它们没有实际意义;您需要进行重大设计更改才能避免此问题)。