Placement new base subobject of derived in C++
Asked Answered
T

2

2

Is it defined behavior to placement-new a trivially destructible base object of a derived?

struct base { int& ref; };
struct derived : public base {
    complicated_object complicated;
    derived(int& r, complicated_arg arg) :
            base {r}, complicated(arg) {}
};

unique_ptr<derived> rebind_ref(unique_ptr<derived>&& ptr,
                               int& ref) {
    // Change where the `ref` in the `base` subobject of
    // derived refers.
    return unique_ptr<derived>(static_cast<derived*>(
        ::new (static_cast<base*>(ptr.release()) base{ref}));
}

Note that I tried to structure rebind_ref to not break any strict aliasing assumptions a compiler might have made.

Transpontine answered 19/10, 2018 at 0:18 Comment(3)
why not use int *ref in this case. derived class may be written assuming that ref never changes.Tarttan
My question is not about rebinding a reference. It is about constructing a "new" value on top of the base subobject of a derived.Transpontine
Reusing memory ends the lifetime of the object.Qnp
G
2

No, this is not allowed by the C++ Standard, for at least two reasons.

The text that sometimes allows a placement-new of a new object into the storage for another object of the same type is found in [basic.life], paragraph 8. The bold emphasis is mine.

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

  • [C++17] the original object was a most derived object of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

  • [C++20 draft 2018-10-09] neither the original object nor the new object is a potentially-overlapping subobject ([intro.object]).

The C++20 change is to account for the possibility of zero-size non-static data members, but it still also rules out all base class subobjects (empty or not). "Potentially-overlapping subobject" is a new term defined in [intro.object] paragraph 7:

A potentially-overlapping subobject is either:

  • a base class subobject, or

  • a non-static data member declared with the no_unique_address attribute ([dcl.attr.nouniqueaddr]).

(Even if you do find some way to rearrange things to avoid the reference member and base class issues, remember to make sure nobody can ever define a const derived variable, for example by making all constructors private!)

Gillett answered 21/10, 2018 at 19:1 Comment(5)
Thank you for the excellent answer. "...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..." is interesting. Is the result of a placement-new expression a "pointer that pointed to the original object", or a pointer to the newly constructed object (despite it occupying the same memory location)?Transpontine
The result of any new-expression, including placement new, points at the created object. That sentence is talking about previously existing pointers and references.Gillett
I carefully structured rebind_ref so that it consumes the "old" pointer and returns the "new" pointer that is the result of the placement-new expression static_casted to derived. Is this sufficient to avoid the "pointer that pointed to the original object" rule?Transpontine
@Transpontine A base class subobject isn't really a "pointer", "reference", or "name". But the Standard doesn't need to clarify how a derived object behaves after replacing a base class subobject, because it says such a replacement is always invalid in the first place.Gillett
Actually, the sections of the standard you quoted above only speak whether a "pointer", "reference", or "name" that referred to the object that previously occupied some storage can be used to manipulate the new object created there. In C++17 I'd use std::launder to "create" a "new name". Unfortunately, I only have access to C++14. Does placement new have std::launder powers before C++17?Transpontine
Q
-1
static_cast<derived*>(
        ::new (&something) base{ref})

is not valid, by definition new (...) base(...) creates a base object as a new complete object, that can be considered an existing complete object or member subobject sometimes (under conditions that aren't met by base anyway) but never a base subobject.

There is no existing rule that says you can pretend new (addr) base creates a valid derived object just because the base object is overwriting another base base subobject. If there previously was a derived object, you have just reused its storage with new (addr) base. Even if by some magic the derived object was still in existence, the result of the evaluation of the new expression wouldn't point to it, it would point to a base complete object.

If you want to pretend you did something (like creating a derived object), without actually doing it (calling a derived constructor), you can pour some volatile qualifiers on pointers to force the compiler to erase all assumptions on values and compile the code as if there was an ABI transition.

Qnp answered 21/10, 2018 at 19:26 Comment(2)
Where would you recommend that I add volatile qualifiers to make what I'm doing not UB without negatively affecting how a compiler can optimize this code?Transpontine
@Transpontine The goal of volatile is to remove information from the compiler: the value of a volatile object seems to be coming from a separately compiled unit (and possibly a different compiler and language). The standard doesn't cover at all separate compilation or mixing different languages, not even C.Qnp

© 2022 - 2024 — McMap. All rights reserved.