12

This is a code example from the C++20 spec ([basic.life]/8):

struct C {
  int i;
  void f();
  const C& operator=( const C& );
};

const C& C::operator=( const C& other) {
  if ( this != &other ) {
    this->~C();              // lifetime of *this ends
    new (this) C(other);     // new object of type C created
    f();                     // well-defined
  }
  return *this;
}

int main() {    
  C c1;
  C c2;
  c1 = c2;   // well-defined
  c1.f();    // well-defined; c1 refers to a new object of type C
}

Would the following be legal or undefined behavior:

struct C {
  int& i; // <= the field is now a reference
  void foo(const C& other) {
    if ( this != &other ) {
      this->~C();  
      new (this) C(other);  
    }
  }
};

int main() {
    int i = 3, j = 5;
    C c1 {.i = i};
    std::cout << c1.i << std::endl;
    C c2 {.i = j};
    c1.foo(c2);
    std::cout << c1.i << std::endl;
}

In case it is illegal, would std::launder make it legal? where should it be added?

Note: p0532r0 (page 5) uses launder for a similar case.

In case it is legal, how can it work without "Pointer optimization barrier" (i.e. std::launder)? how do we avoid the compiler from caching the value of c1.i?

The question relates to an old ISO thread regarding Implementability of std::optional.

The question applies also, quite similarly, to a constant field (i.e. if above i in struct C is: const int i).


EDIT

It seems, as @Language Lawyer points out in an answer below, that the rules have been changed in C++20, in response to RU007/US042 NB comments.

C++17 Specifications [ptr.launder] (§ 21.6.4.4): --emphasis mine--

[ Note: If a new object is created in storage occupied by an existing object of the same type, a pointer to the original object can be used to refer to the new object unless the type contains const or reference members; in the latter cases, this function can be used to obtain a usable pointer to the new object. ...— end note ]

C++17 [ptr.launder] code example in the spec (§ 21.6.4.5):

struct X { const int n; };
X *p = new X{3};
const int a = p->n;
new (p) X{5}; // p does not point to new object (6.8) because X::n is const
const int b = p->n; // undefined behavior
const int c = std::launder(p)->n; // OK

C++20 [ptr.launder] Specifications (§ 17.6.4.5):

[ Note: If a new object is created in storage occupied by an existing object of the same type, a pointer to the original object can be used to refer to the new object unless its complete object is a const object or it is a base class subobject; in the latter cases, this function can be used to obtain a usable pointer to the new object. ...— end note ]

Note that the part:

unless the type contains const or reference members;

that appeared in C++17 was removed in C++20, and the example was changed accordingly.

C++20 [ptr.launder] code example in the spec (§ 17.6.4.6):

struct X { int n; };
const X *p = new const X{3};
const int a = p->n;
new (const_cast<X*>(p)) const X{5}; // p does not point to new object ([basic.life])
                                    // because its type is const
const int b = p->n;                 // undefined behavior
const int c = std::launder(p)->n;   // OK

Thus, apparently the code in question is legal in C++20 as is, while with C++17 it requires using std::launder when accessing the new object.


Open Questions:

  • What is the case of such code in C++14 or before (when std::launder didn't exist)? Probably it is UB - this is why std::launder was brought to the game, right?

  • If in C++20 we do not need std::launder for such a case, how the compiler can understand that the reference is being manipulated without our help (i.e. without "Pointer optimization barrier") to avoid caching of the reference value?


Similar questions here, here, here and here got contradicting answers, some see that as a valid syntax but advise to rewrite it. I'm focusing on the validity of the syntax and the need (yes or no) for std::launder, in the different C++ versions.

Amir Kirsh
  • 12,564
  • 41
  • 74
  • You can't call `this->~C()` on a `C` object that wasn't constructed with `placement-new` (ie, the `c1` variable in your 1st example's `main()`). And having `operator=` call `placement-new` won't let outside code know that they now need to call `~C()` explicitly. So your code is full of UB. Just don't do it. Have your `operator=` use the copy-swap idiom instead, that will allow you to deal with the reference member properly by defining a proper copy constructor for `C`. – Remy Lebeau Jun 02 '20 at 21:31
  • @RemyLebeau I believe code example from the C++ spec, which is not intended for presenting UB, would not fall into UB. But you can never know. – Amir Kirsh Jun 02 '20 at 21:40
  • I'm no expert on the standard spec, but I seriously doubt it would promote examples like you have shown, since they are doing improper things to the objects. – Remy Lebeau Jun 02 '20 at 22:30
  • @RemyLebeau I agree that this is an unorthodox way of doing things, but in some rare cases _desperate times may call for desperate measures._ See the ISO discussion I refer to at the end. Anyway I added the proper tag to bring in the _language lawyers._ – Amir Kirsh Jun 03 '20 at 00:15
  • @RemyLebeau *"You can't call `this->~C()` on a `C` object that wasn't constructed with `placement-new`"* Is it actually forbidden? I always thought it could be problematic only if the class requires laundering (e.g. has member references; because after that you'd have to `launder` every use of the object). – HolyBlackCat Jun 03 '20 at 00:16
  • A member `const int i;` would have similar issues - but is there a relevant difference there? At first glance, it looks like a compiler can't assume a reference member of the same object refers to the same object/function, and can't assume a `const` member of the same object has the same value across function calls without visible definition. But I imagine that would be bad for optimizations. – aschepler Jun 03 '20 at 00:38

2 Answers2

6

It is legal to replace objects with const-qualified and reference non-static data members. And now, in C++20, [the name of|a [pointer|reference] to] the original object will refer to the new object after replacement. The rules has been changed in response to RU007/US042 NB comments http://wg21.link/p1971r0#RU007:

RU007. [basic.life].8.3 Relax pointer value/aliasing rules

...

Change 6.7.3 [basic.life] bullet 8.3 as follows:

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 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 neither a complete object that is const-qualified nor a subobject of such an object, and

  • ...

Language Lawyer
  • 3,378
  • 1
  • 12
  • 29
  • So this is new in C++20 and illegal before that? and how can it work without "Pointer optimization barrier" (i.e. std::launder)? – Amir Kirsh Jun 03 '20 at 01:36
  • @AmirKirsh It is legal before C++20, but names/pointers/references didn't rebind to the new object. – Language Lawyer Jun 03 '20 at 02:01
  • So what is the expected result before C++20? That the reference field would keep it's initial value!? Or there was a need for launder in C++17 and not in C++20? (in which case is it valid or UB, in C++14 and before?) And how can it avoid caching of the reference value without helping the compiler understand that the reference is being manipulated? – Amir Kirsh Jun 03 '20 at 04:35
  • 1
    I'm also interested in what this means for `std::launder` or other workarounds for such cases. – underscore_d Jun 03 '20 at 13:22
  • @AmirKirsh In the case of the object model, compiler code are not an implementation of the standard. This is the opposite, the standard is an attempt to express the common behavior of compilers while compilers try to ensure backward compatibility so that old code can still be compiled, even with new standard. The concequence is that whatever the standard you select on the command line, you can expect that the object model followed by the compiler is closer to what is expressed in the c++20 standard than in the C++17 or C++14 standards. With a notable exception: implicit-life time objects. – Oliv Jun 03 '20 at 17:01
1

To answer the currently open questions:

First question:

  • What is the case of such code in C++14 or before (when std::launder didn't exist)? Probably it is UB - this is why std::launder was brought to the game, right?

Yes, it was UB. This is mentioned explicitly in the NB issues @Language Lawyer referred to:

Because of that issue all the standard libraries have undefined behaviors in widely used types. The only way to fix that issue is to adjust the lifetime rules to auto-launder the placement new. (https://github.com/cplusplus/nbballot/issues/7)

Second question:

If in C++20 we do not need std::launder for such a case, how the compiler can understand that the reference is being manipulated without our help (i.e. without "Pointer optimization barrier") to avoid caching of the reference value?

Compilers already know to not optimize object (or sub-object) value this way if a non-const member function was called between two usages of the object or if any function was called with the object as a parameter (passed by-ref), because this value may be changed by those functions. This change to the standard just added a few more cases where such optimization is illegal.

Yehezkel B.
  • 1,140
  • 6
  • 10