Brian Bi
Brian Bi

Reputation: 119239

What exactly constitutes the "name" of an object for the purposes of object replacement?

According to [basic.life]/8,

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
  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
  • the original object was a most derived object (4.5) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

... [ Note: If these conditions are not met, a pointer to the new object can be obtained from a pointer that represents the address of its storage by calling std::launder (21.6). — end note ]

The standard contains an example that demonstrates that when there is a const subobject, "the name of the original object" fails to refer to the new object, and use of that name causes UB. It is located in [intro.object]/2:

Objects can contain other objects, called subobjects. A subobject can be a member subobject (12.2), a base class subobject (Clause 13), or an array element. An object that is not a subobject of any other object is called a complete object. If an object is created in storage associated with a member subobject or array element e (which may or may not be within its lifetime), the created object is a subobject of e’s containing object if:

  • the lifetime of e’s containing object has begun and not ended, and
  • the storage for the new object exactly overlays the storage location associated with e, and
  • the new object is of the same type as e (ignoring cv-qualification).

[ Note: If the subobject contains a reference member or a const subobject, the name of the original subobject cannot be used to access the new object (6.8). — end note ] [ Example:

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
}

However, it would seem to me that the fact that [basic.life]/8 doesn't give the lvalue-to-rvalue conversion on u.x.n defined behaviour is irrelevant, because it is given defined behaviour by [expr.ref]/4.2, which has the following to say about the class member access expression E1.E2:

If E2 is a non-static data member and the type of E1 is “cq1 vq1 X”, and the type of E2 is “cq2 vq2 T”, the expression designates the named member of the object designated by the first expression. ...

My reading of this is that the expression u.x yields an lvalue referring to the current x subobject of whatever object u currently refers to. Since, according to [intro.object]/2, the creation of the new X object in the place of u.x causes the new X object to actually be a subobject of u, performing an lvalue-to-rvalue conversion on u.x.n should be well-defined.

If we assume that the UB in this example reflects the intent of the standard, it appears that we must read [basic.life]/8 as saying that notwithstanding the fact that certain expressions may appear to access the new object (in this case, due to [expr.ref]/4.2), they are nonetheless UB if they attempt the access using the original object's "name". (Or, in practical terms, that the compiler may assume that the "name" continues to refer to the original object, and thus not re-read the const member's value.)

Normally, though, I would not think that u.x counts as "naming" the X subobject of u, because I think that subobjects do not have names. Thus, [basic.life]/8 appears to be saying that UB occurs in some particular situations but without precisely explaining what those situations are.

Thus, my questions are:

  1. Am I correct in saying that [basic.life]/8 causes this example to contain UB rather than simply failing to give it defined behaviour?
  2. Is there a precise specification of which cases are given UB by [basic.life]/8?
  3. Should the standard be reworded to be more clear on when [basic.life]/8 causes UB (i.e., when std::launder is needed)?

Upvotes: 4

Views: 161

Answers (1)

Nicol Bolas
Nicol Bolas

Reputation: 473537

My reading of this is that the expression u.x yields an lvalue referring to the current x subobject of whatever object u currently refers to.

And that's true. What you misunderstand is what "the current x subobject" means.

When you do new (&u.x) X {2}, that creates a new object at the address of u.x. However, nowhere in the standard does it say that this object is named x or u.x. Yes, it is a subobject of u, but it has no name. Nowhere does the standard say that the newly created subobject has that name, or any name for that matter.

Indeed, if what you said was true, [basic.life]/8 and launder wouldn't need to exist at all, since an object in overlaid storage of another object would always be accessible through the name of the old object.

Normally, though, I would not think that u.x counts as "naming" the X subobject of u, because I think that subobjects do not have names.

I don't know how you came to that conclusion, since you quoted a part of the specification that clearly states that member subobjects can have names:

the expression designates the named member of the object

Emphasis added. That clearly suggests to me that the member subobject has a name, and the expression u.x designates the name of that particular member subobject (not merely any member subobject in that address of the appropriate type).

That is, just as u refers specifically to the object which was declared by the declaration of u, u.x refers specifically to the x-named member of the object declared by the declaration u.

Upvotes: 3

Related Questions