1

Is it allowed to reuse storage of a non-static data member and if so under what conditions?

Consider the program

#include<new>
#include<type_traits>

using T = /*some type*/;
using U = /*some type*/;

static_assert(std::is_object_v<T>);
static_assert(std::is_object_v<U>);
static_assert(sizeof(U) <= sizeof(T));
static_assert(alignof(U) <= alignof(T));

struct A {
    T t /*initializer*/;
    U* u;

    A() {
        t.~T();
        u = ::new(static_cast<void*>(&t)) U /*initializer*/;
    }

    ~A() {
        u->~U();
        ::new(static_cast<void*>(&t)) T /*initializer*/;
    }

    A(const A&) = delete;
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;
};

int main() {
    auto a = new A;
    *(a->u) = /*some assignment*/;
    delete a; /*optional*/

    A b; /*alternative*/
    *(b.u) = /*some assignment*/; /*alternative*/
}

What conditions do object types T and U need to satisfy in addition to the static_asserts, so that the program has defined behavior, if any?

Does it depend on the destructor of A actually being called (e.g. on whether the /*optional*/ or /*alternative*/ lines are present)?.

Does it depend on the storage duration of A, e.g. whether /*alternative*/ lines in main are used instead?


Note that the program does not use the t member after the placement-new, except in the destructor. Of course using it while its storage is occupied by a different type is not allowed.


Please also note that I do not encourage anyone to write code like that. My intention is to understand details of the language better. In particular I did not find anything forbidding such placement-news as long as the destructor is not called, at least.


See also my other question regarding a modified version that does not execute the placement-news during construction/destruction of the enclosing object, since that seems to have caused complications according to some comments.


Concrete example as requested in comments demonstrating the wider question for a subset of types that I think represent different cases of interest:

#include<new>
#include<type_traits>

struct non_trivial {
    ~non_trivial() {};
};

template<typename T, bool>
struct S {
    T t{};
    S& operator=(const S&) { return *this; }
};

template<bool B>
using Q = S<int, B>; // alternatively S<const int, B> or S<non_trivial, B>

using T = Q<true>;
using U = Q<false>;

static_assert(std::is_object_v<T>);
static_assert(std::is_object_v<U>);
static_assert(sizeof(U) <= sizeof(T));
static_assert(alignof(U) <= alignof(T));

struct A {
    T t;
    U* u;

    A() {
        t.~T();
        u = ::new(static_cast<void*>(&t)) U;
    }

    ~A() {
        u->~U();
        ::new(static_cast<void*>(&t)) T;
    }

    A(const A&) = delete;
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;
};

int main() {
    auto a = new A;
    *(a->u) = {};
    delete a; /*optional*/

    // A b; /*alternative*/
    // *(b.u) = {}; /*alternative*/
}
walnut
  • 21,629
  • 4
  • 23
  • 59
  • @JesperJuhl I don't and I do not encourage anyone to do so. I want to understand some corner cases of the language better. – walnut Dec 08 '19 at 17:01
  • @LanguageLawyer That makes no sense. How would you restart the lifetime of `*this`? – curiousguy Dec 08 '19 at 17:12
  • @curiousguy Ehm... I was gonna restart the lifetime of `*this`? – Language Lawyer Dec 08 '19 at 17:13
  • @LanguageLawyer So you can't end the lifetime of a data member w/o completely and forever destroying the existence of the whole instance? Again that makes no sense. It can't be. It may be what the text of the std says, but the std isn't authoritative. – curiousguy Dec 08 '19 at 17:14
  • @LanguageLawyer Do you want to write this up as an answer? Having looked at the standard references you are likely basing this on, I think that you are right. – walnut Dec 08 '19 at 17:18
  • @curiousguy _"So you can't end the lifetime of a data member w/o completely and forever destroying the existence of the whole instance?"_ No, I was not saying this. – Language Lawyer Dec 08 '19 at 17:18
  • @LanguageLawyer Under what conditions can you end the lifetime of a member? – curiousguy Dec 08 '19 at 17:19
  • @curiousguy without ending the lifetime of the enclosing object? – Language Lawyer Dec 08 '19 at 17:19
  • @LanguageLawyer Without forever ending its lifetime – curiousguy Dec 08 '19 at 17:20
  • @curiousguy see my first comment. – Language Lawyer Dec 08 '19 at 17:21
  • @walnut I think "U shall be the same type as T (up to cv-qualifiers)" is not necessary, saying about nested within is enough. – Language Lawyer Dec 08 '19 at 17:23
  • @LanguageLawyer Your position doesn't make sense. Please provide code. – curiousguy Dec 08 '19 at 17:26
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/203847/discussion-between-curiousguy-and-language-lawyer). – curiousguy Dec 08 '19 at 17:28
  • 1
    @LanguageLawyer Yes, I thought that there might be a possibility that doing this in the constructor causes additional issues. I should have provided an alternative question where both placement-news are done outside the constructor/destructor. The main point that was important to me was however answered by your comments so far, namely that unrelated types with the exception of nesting are not allowed. I did not realize the effect on the enclosing object. – walnut Dec 08 '19 at 17:34
  • @walnut I won't try to answer now, because you try to reuse the storage during construction or destruction, which makes the analysis slightly harder. – Language Lawyer Dec 08 '19 at 17:42
  • Please provide a [mre] by replacing the `/*some type*/`-like comments with actual compilable code. Sometimes details like this can have a surprising effect on the conclusion. – L. F. Dec 09 '19 at 08:52
  • @L.F. I have added a concrete example, although I want to actually know (at least roughly) what the conditions on the types are in general. Please consider the alternatives for `using Q = S;` in the comment following it, though. – walnut Dec 09 '19 at 17:25

1 Answers1

-1

That looks ok, with some problems depending on the contents of T or U, or if T::T throws.

From cppreference

If a new object is created at the address that was occupied by another object, then all pointers, references, and the name of the original object will automatically refer to the new object and, once the lifetime of the new object begins, can be used to manipulate the new object, but only if the following conditions are satisfied:

  • the storage for the new object exactly overlays the storage location which the original object occupied
  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers)
  • the type of the original object is not const-qualified
  • if the original object had class type, it does not contain any non-static data member whose type is const-qualified or a reference type
  • 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).

And you must GUARANTEE the new object is created, including in exceptions.

Directly from the standard:

[basic.life] 6.8/8:

(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:

  • (8.1) the storage for the new object exactly overlays the storage location which the original object occupied, and
  • (8.2) the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
  • (8.3) 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
  • (8.4) 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).

That applies when T is U basically.

As for reusing space for T with a different U then backfilling:

[basic.life] 6.8/9:

(9) If a program ends the lifetime of an object of type T with static, thread, or automatic storage duration and if T has a non-trivial destructor,the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined. This is true even if the block is exited with an exception.

And T (nor U) cannot contain anything non-static const.

[basic.life] 6.8/10:

(10) Creating a new object within the storage that a const complete object with static, thread, or automatic storage duration occupies, or within the storage that such a const object used to occupy before its lifetime ended, results in undefined behavior.

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Note that this speaks about the situation **not** during construction/destruction. And cppreference is not the standard. – Language Lawyer Dec 08 '19 at 17:49
  • 1
    @LanguageLawyer: During construction/destruction of what? By the time the body of the outer constructor has been called, the subobjects have all been constructed and therefore are live, valid objects. And those objects remain live during the destructor. – Nicol Bolas Dec 08 '19 at 17:56
  • @NicolBolas plz see https://timsong-cpp.github.io/cppwp/n4659/basic.life#7. It says "For an object under construction or destruction, see [class.cdtor]". [class.cdtor] doesn't seem to allow the reuse the storage of non-static data members. – Language Lawyer Dec 08 '19 at 17:58
  • @LanguageLawyer: I don't see how that section applies to this case. The subobject is not the object under construction or destruction at that point. So I see nothing which explicitly forbids reusing the storage of an object which already exists. – Nicol Bolas Dec 08 '19 at 18:00
  • @NicolBolas but `*this` is under construction/destruction. Lets say, `T` = `int` and `U` = `float`. Will the lifetime of `*this` start after the return from the constructor if I create a `float` object inside the storage of the `int` non-static data member in the constructor? – Language Lawyer Dec 08 '19 at 18:01
  • @NicolBolas Even simpler: if `T` = `U` = `int`? – Language Lawyer Dec 08 '19 at 18:02
  • @LanguageLawyer: Exceptions aside, the lifetime of `*this` will begin *regardless* of what you do in the constructor. That's well stated in [basic.life]/1. – Nicol Bolas Dec 08 '19 at 18:03
  • @NicolBolas _the lifetime of `*this` will begin regardless of what you do in the constructor_ Orly? Even if I do `::new (this) std::remove_reference_t;`? – Language Lawyer Dec 08 '19 at 18:03
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/203851/discussion-between-nicol-bolas-and-language-lawyer). – Nicol Bolas Dec 08 '19 at 18:04
  • @Yakk Outside of the destructor I am never using `t` again, so it doesn't matter whether it refers to the new object. Do you think that conditions you quote must be satisfied even if the `/*optional*/` and `/*alternative*/` lines are not present? – walnut Dec 08 '19 at 18:10
  • 1
    @LanguageLawyer The subobject `t` is not under construction/destruction at the time the OP is doing things. `A` is under construction/destruction. – Yakk - Adam Nevraumont Dec 08 '19 at 18:15
  • 1
    @Yakk-AdamNevraumont Regarding your last quote: This only applies to the storage of *complete objects*. `t` and I assume also the newly created objects would be subobjects of `a` and therefore not complete. – walnut Dec 08 '19 at 18:28
  • 2
    "*An object that is not a subobject of any other object is called a complete object.*": https://eel.is/c++draft/basic#intro.object-2 – walnut Dec 08 '19 at 18:29
  • @walnut Ah, I got confused with "complete classes". :) But searching for "complete objects" I'm not seeing why those clauses only apply to them. – Yakk - Adam Nevraumont Dec 08 '19 at 18:31
  • I am also uncertain under what conditions the second-to-last quote applies here. It certainly does not apply for the dynamically created `A` and for the automatic storage duration `A`, I am purposefully constructing an object of the original type in the destructor. Absent exceptions in that construction, I am wondering if the quoted passage is satisfied. – walnut Dec 08 '19 at 18:33
  • @walnut Yes, it states it applies for objects with "with static, thread, or automatic storage duration". It is interesting what happens when the subobject lifetime is ended when the containing complete object in the free store is deleted. I'd assume that there is going to be a clause about that in the standard, but I have not found it. – Yakk - Adam Nevraumont Dec 08 '19 at 18:38
  • @walnut _"`t` and I assume also the newly created objects would be subobjects of `a`"_ The object you placement-new-create in the constructor will never become a subobject of `a`, because the lifetime of `a` hasn't started yet. See http://eel.is/c++draft/intro.object#2.1 and above. – Language Lawyer Dec 08 '19 at 19:10
  • @LanguageLawyer Then, before the contained object's lifetime has begun, the "suboject" is not a suboject, and objects that are not subobjects are complete objects. – Yakk - Adam Nevraumont Dec 08 '19 at 19:56
  • @Yakk-AdamNevraumont What do you think about https://timsong-cpp.github.io/cppwp/n4659/intro.object#8? Will we have 2 complete objects within their lifetime none of which is nested within the other when the constructor returns? – Language Lawyer Dec 08 '19 at 20:07
  • @LanguageLawyer Remove the placement new. By the standard, `t` *is not a subobject* during the ctor of `A`. Because it can only be a subobject if the lifetime of the containing object has begun, and that happens at the end of the ctor of `A`. At the end of the `A` ctor, `t` is no longer a complete object, but is now a subobject. I posit the same thing happens to the placement object created in the storage of `t`. – Yakk - Adam Nevraumont Dec 08 '19 at 20:17