出于对象替换的目的,究竟是什么构成对象的“名称”?

时间:2019-02-10 04:03:04

标签: c++ language-lawyer

根据[basic.life] / 8,

  

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

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

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

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

  

对象可以包含其他对象,称为 subobjects 。子对象可以是成员子对象(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的类型为“ cq1 vq1 X”,并且E2的类型为“ cq2 vq2 T”,   该表达式指定第一个表达式指定的对象的命名成员。 ...

我的理解是,表达式u.x产生一个左值,该值引用当前对象x所引用的 current u子对象。因为根据[intro.object] / 2,在X处创建新的u.x对象会导致新的X对象实际上是{{1 }},应该明确定义u上的左值到右值转换。

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

但是,通常我不会认为const被视为“命名” u.x的{​​{1}}子对象,因为我认为子对象没有名称。因此,[basic.life] / 8似乎在说UB在某些特定情况下发生,但没有确切解释这些情况是什么。

因此,我的问题是:

  1. 我是否正确地说[basic.life] / 8 导致这个示例包含UB,而不是简单地未能给出定义的行为?
  2. 是否有[em> 用[basic.life] / 8给出UB的案例的精确规范?
  3. 在[basic.life] / 8导致UB时(何时需要X)时,是否应该对标准重新措词以更清楚?

1 个答案:

答案 0 :(得分:3)

  

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

是的。您误会的是“当前x子对象”的含义。

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

实际上,如果您说的是正确的,则[basic.life] / 8和launder根本不需要存在,因为始终可以通过名称访问另一个对象的重叠存储中的对象的旧物体。

  

不过,通常我不会认为u.x被视为“命名” X的{​​{1}}子对象,因为我认为这些子对象没有名称。

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

  

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

已添加重点。显然,这向我暗示成员子对象具有名称,而表达式u指定该特定成员子对象的名称(不仅仅是该地址中具有适当类型的任何成员子对象)。

也就是说,就像u.x专门指代u声明所声明的对象一样,u专门指代u.x的命名成员。由声明x声明的对象。