不死对象 ([basic.life]/8):为什么允许引用重新绑定(和常量修改)?

cur*_*guy 6 c++ constants lifetime placement-new language-lawyer

“亡灵”条款

我将不死子句称为 C++ 规则,即在对象销毁后,如果在同一地址创建新对象,有时可以将其视为与旧对象相同的对象。该规则始终存在于 C++ 中,但对附加条件进行了一些更改。

这个问题让我阅读了最新的不死条款。Lifetime [basic.life]/8中修改后的条件是:

(8.1) 新对象的存储正好覆盖原对象所占用的存储位置,并且

嗯,呵呵。位于不同地址的对象不会是同一个对象。

(8.2) 新对象与原始对象的类型相同(忽略顶级 cv 限定符),以及

再说一遍,呵呵。

(8.4) 原始对象和新对象都不是潜在重叠的子对象 ([intro.object])。

它不能是基类、经典类(或具有使其地址不唯一的特殊声明的成员)。再说一遍,呵呵。

(8.3) 原始对象既不是 const 限定的完整对象,也不是此类对象的子对象,并且

现在这很有趣。被替换的对象不能是:

  • 一个完整的 const 对象
  • 完整 const 对象的一部分

另一方面,被复活的对象可以是:

  1. const 成员子对象
  2. 这种 const 成员的子对象
  3. const 对象数组中的一个元素

常量子对象

所以在我看来,所有这些对象x都可以复活:

常量成员子对象

struct CI {
  const int x;
};

CI s = { 1 };
new ((void*)&s.x) int(2);
int r = s.x; // OK, 2
Run Code Online (Sandbox Code Playgroud)

const 成员的子对象:

struct T {
  int x;
};

struct CT {
  const T m = { 1 };
};

CT s;
new ((void*)&s.m.x) int (2);
int r = s.m.x;
Run Code Online (Sandbox Code Playgroud)

const 对象数组中的元素:

const int x[1] = { 1 };
new ((void*)&x[0]) int (2);
int r = x[0];
Run Code Online (Sandbox Code Playgroud)

具有 const 和引用成员的类

具有 const 或引用成员的类类型的对象似乎也没有被禁止;复活的对象仍然被称为x

具有 const 成员的类:

struct CIM {
  CIM(int i): m(i) {}
  const int m;
};

CIM x(1);
new ((void*)&x) CIM(2);
int r = x.m; // OK, 2
Run Code Online (Sandbox Code Playgroud)

具有引用成员的类:

struct CRM {
  CRM (int &r): m(r) {}
  int &m;
};

int i=1,j=2;
CRM x(i);
new ((void*)&x) CRM(j);
int r = x.m; // OK, 2
Run Code Online (Sandbox Code Playgroud)

问题

  1. 对条款的这种解释是否正确?
  2. 如果是这样,是否还有其他条款禁止这些覆盖操作?
  3. 如果是这样,这是故意的吗?为什么要改?
  4. 这是代码生成器的重大变化吗?所有编译器都真的支持吗?他们不是基于 const 成员进行优化,数组的 const 元素是不可变的并且引用不可重新绑定吗?
  5. 额外问题:这是否会影响具有足够存储类(当然不是动态创建的对象)和足够初始化的 const 对象的 ROM 能力?

注意:我后来添加了奖励,因为在讨论中提到了将常量放入 ROM。

Oli*_*liv 1

如果与对象生命周期相关的标准的所有要求都不在[基本生命周期]中,那将是令人惊讶的。

在您引用的标准段落中,“完整”形容词被无意中添加到“对象”名称中的可能性很小。

在论文P0137中,人们可以阅读这一理性(下面@LanguageLawyer 评论中引用的论文):

这是允许 std::Optional 等类型包含 const 子对象所必需的;现有的限制是为了允许 ROMability,因此只影响完整的对象。

为了让我们放心,我们可以验证编译器确实遵循字母中的标准措辞:它们对完整 const 对象执行常量优化,但不对非 const 完整对象的 const 成员子对象执行常量优化:

让我们考虑一下这段代码

struct A{const int m;};

void f(const int& a);

auto g(){
    const int x=12;
    f(x);
    return x;
}

auto h(){
    A a{12};
    f(a.m);
    return a.m;
}
Run Code Online (Sandbox Code Playgroud)

当面向 x86_64 时,Clang 和 GCC 都会生成此程序集:

g():                                  # @g()
        push    rax
        mov     dword ptr [rsp + 4], 12
        lea     rdi, [rsp + 4]
        call    f(int const&)
        mov     eax, 12     ;//the return cannot be anything else than 12
        pop     rcx
        ret
h():                                  # @h()
        push    rax
        mov     dword ptr [rsp], 12
        mov     rdi, rsp
        call    f(int const&)
        mov     eax, dword ptr [rsp]  //the content of a.m is returned
        pop     rcx
        ret
Run Code Online (Sandbox Code Playgroud)

返回的值放置在寄存器中eax(根据 ABI 规范:System V x86 处理器特定 ABI):

  • 在函数中,g编译器可以自由地假设x不能在调用时更改,f因为x它是一个完整的 const 对象。因此该值作为立即数 12直接放入寄存器中: 。eaxmov eax, 12

  • 在函数中,h编译器不能随意假设a.m不能在调用时更改,f因为a.m它不是完整 const 对象的子对象。所以调用后f的值a.m必须从内存加载到eaxmov eax, dword ptr [rsp]

  • _“很少有机会无意中添加了“完整”形容词”_ http://wg21.link/p0137r1 (2认同)