首页 > 解决方案 > 出于对象替换的目的,对象的“名称”究竟是什么?

问题描述

根据[basic.life]/8,

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

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

... [注意:如果不满足这些条件,则可以通过调用std::launder(21.6)从表示其存储地址的指针中获得指向新对象的指针。——尾注]

该标准包含一个示例,该示例演示了当存在const子对象时,“原始对象的名称”无法引用新对象,并且使用该名称会导致 UB。它位于 [intro.object]/2 中:

对象可以包含其他对象,称为子对象。子对象可以是成员子对象(12.2)、基类子对象(第 13 条)或数组元素。不是任何其他对象的子对象的对象称为完整对象。如果在与成员子对象或数组元素e关联的存储中创建对象(可能在也可能不在其生命周期内),则创建的对象是e的包含对象的子对象,如果:

  • e的包含对象的生命周期已经开始但没有结束,并且
  • 新对象的存储正好覆盖与e关联的存储位置,并且
  • 新对象与e的类型相同(忽略 cv 限定)。

[注意:如果子对象包含引用成员或const子对象,则不能使用原始子对象的名称访问新对象(6.8)。—尾注] [示例:

struct X { const int n; };
union U { X x; float f; };
void tong() {
  U u = {{ 1 }};
  u.f = 5.f;                          // OK, creates new subobject of u (12.3)
  X *p = new (&u.x) X {2};            // OK, creates new subobject of u
  assert(p->n == 2);                  // OK
  assert(*std::launder(&u.x.n) == 2); // OK
  assert(u.x.n == 2);                 // undefined behavior, u.x does not name new subobject
}

然而,在我看来,[basic.life]/8 没有对u.x.n定义的行为进行左值到右值转换的事实是无关紧要的,因为它是由 [expr.ref]/4.2 给出的定义行为,它对类成员访问表达式有以下说法E1.E2

如果E2是非静态数据成员并且类型E1是“<em>cq1 vq1 X ”,并且类型E2是“<em>cq2 vq2 T ”,则表达式指定由第一个表达式指定的对象的命名成员。...

我对此的理解是,该表达式u.x产生一个左值,该左值引用当前引用的任何对象的当前 子对象。由于根据 [intro.object]/2,在的 位置创建新对象会导致新对象实际上是 的子对象,因此应该明确定义对 执行左值到右值的转换。xuXu.xXuu.x.n

如果我们假设这个例子中的 UB 反映了标准的意图,那么我们似乎必须阅读 [basic.life]/8 的意思是,尽管某些表达式可能会出现访问新对象的事实(在这种情况下,由于 [expr.ref]/4.2),如果他们尝试使用原始对象的“名称”进行访问,它们仍然是 UB。(或者,实际上,编译器可能假设“名称”继续引用原始对象,因此不会重新读取const成员的值。)

不过,通常情况下,我不认为这u.x算作“命名” 的X子对象u,因为我认为子对象没有名称。因此,[basic.life]/8 似乎是在说 UB 发生在某些特定情况下,但没有准确解释这些情况是什么。

因此,我的问题是:

  1. 我是否正确地说 [basic.life]/8导致此示例包含 UB 而不是简单地没有给它定义的行为?
  2. [basic.life]/8 是否对 UB 给出了哪些情况的精确说明?
  3. 是否应该重新编写标准以更清楚地说明 [basic.life]/8 何时导致 UB(何时std::launder需要)?

标签: c++language-lawyer

解决方案


我对此的理解是,该表达式u.x产生一个左值,该左值引用当前引用x的任何对象的当前子对象u

这是真的。您误解的是“当前x子对象”的含义。

执行此操作new (&u.x) X {2}时,会在 的地址处创建一个新对象u.x。但是,标准中没有任何地方说这个对象被命名为xor u.x。是的,它是 的子对象u,但它没有名称。标准没有说新创建的子对象具有该名称或任何名称。

确实,如果您说的是真的, [basic.life]/8 并且launder根本不需要存在,因为始终可以通过旧对象的名称访问另一个对象的覆盖存储中的对象。

不过,通常情况下,我不认为这u.x算作“命名” 的X子对象u,因为我认为子对象没有名称。

我不知道你是怎么得出这个结论的,因为你引用了规范的一部分,明确指出成员子对象可以有名称:

表达式指定对象的命名成员

重点补充。这清楚地表明成员子对象有一个名称,并且表达式u.x指定了该特定成员子对象的名称(不仅仅是该地址中适当类型的任何成员子对象)。

也就是说,正如u特指由 的声明所声明的对象一样uu.x特指由声明所声明x的对象的 -named 成员u


推荐阅读