3

The "Using placement new to update a reference member?" question shows this example (simplified):

struct Foo { int& v_; };

int a, b;
Foo f{a};

new (&f) Foo{b};

assert(&f.v_ == &a); // UB

Accessing f through its original name is definitely UB as explained by T.C. in the linked question. I know that std::launder can be used to work around this:

assert(&std::launder(&f)->v_ == &a); // OK, will fire the assert

But what about using the pointer returned by placement new? I.e.

auto p = new (&f) Foo{b};    
assert(&(p->v_) == &a); // UB? OK?

In this case, we're not referring to the object through its original name, but through whatever placement new returned.

Is this undefined behavior or allowed by the Standard?

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • Your code is undefined behavior because the destructor is called through the original name. For a local with automatic storage duration, there's no preventing this. – Ben Voigt Oct 15 '18 at 16:09
  • @BenVoigt: would the behavior be defined if I were to manually control the lifetime of the object? (e.g. using a `std::aligned_storage_t`) – Vittorio Romeo Oct 15 '18 at 16:14
  • `f.~Foo(); auto p = new (&f) Foo{b};` should be fine. – Jarod42 Oct 15 '18 at 16:27
  • @BenVoigt Is this true? We know local objects will get destroyed automatically, but does standard explicitly say that their dtors will be called "through the original name"? – C.M. Oct 15 '18 at 19:43
  • @C.M.: No, there's a parallel rule with exactly the same restrictions as exist for use of the original name. So it is UB, no question. – Ben Voigt Oct 15 '18 at 23:47
  • @Jarod42: Not fine, the automatic destructor call will still occur on the wrong object. An explicit destructor call doesn't inhibit the automatic call. – Ben Voigt Oct 15 '18 at 23:48
  • @VittorioRomeo: That would help; it's only a problem when there is an automatic destructor call. – Ben Voigt Oct 15 '18 at 23:49
  • @BenVoigt can you refer me to related place in standard? (or at least give me a pointer (lol) where to look for it) – C.M. Oct 16 '18 at 00:03
  • @C.M.: Turns out you have to fill in some of the gaps. `[stmt.jump]` and `[stmt.dcl]` make it clear that the "variable" or "object with automatic storage duration" is automatically destroyed. `[basic.life]` tells us that things will be ok if a new object of the same type is substituted for the original, at the time when execution leaves the scope. And the rule for use of old names/pointers/references tells us that objects containing const or reference members cannot be substituted. Together, this equates to UB. – Ben Voigt Oct 16 '18 at 00:09
  • 1
    @C.M.: Working out the meaning of the various rules gets much easier if you consider they are structured to permit constant propagation on `const` data members, and inlining usages of reference data members. – Ben Voigt Oct 16 '18 at 00:17
  • @BenVoigt So, it is ok to replace local object with another one (of the same type) as long as it doesn't contain const or reference members? I could see why you wouldn't want to allow replacing references, but const? constness is essentially a gimmick... Can you elaborate a bit how you came up to a conclusion about const members? (with references to standard, if possible :)) – C.M. Oct 16 '18 at 01:46
  • @C.M.: Follow the second link in the question... And the reasoning is that `struct { int a; const int b; } x = { 1, 42 }; f(x); cout << x.b;` the compiler can avoid reading `x.b` and just do `cout << 42;` If instead of `cout` that variable was controlling a loop, allowing the compiler to unroll... it's a rather big deal to treat `const` objects with constant initializers as actually constant. And between this rule and the `const_cast` restrictions, the compiler actually can. – Ben Voigt Oct 16 '18 at 03:28

1 Answers1

5

This:

auto p = new (&f) Foo{b};    
assert(&(p->v_) == &a); // UB? OK?

Is well-defined. The assert will trigger. new creates a new object, and p points to that new object. That is all perfectly fine. We're reusing f's storage, and there are a lot of rules in [basic.life] about what is OK and not OK about how you can use old names - there are rules about how you can use f, previous pointers to f, etc. You cannot reuse &f without launder-ing it. There rules about how and when you can call destructors in this case, or what about reusing storage for static storage or const objects - none of which matter here either.

But p is a new thing - it just refers to the new object. And p->v_ is the new int& that you created that refers to b. That is not the same object as a, so the pointers compare unequal.

Barry
  • 286,269
  • 29
  • 621
  • 977