Search code examples
c++language-lawyerc++20strict-aliasing

In C++ can you use one empty type as storage for another?


In C++, with reference to the standard, is it safe to use placement new to obtain a pointer to a struct with no members, using another struct with no members as storage? Something like the following:

struct S1 {};
struct S2 {};

S1 s1;
S2* s2 = new (&s1) S2;

The intuitive reason I might expect this to work is that you "can't do anything" with the pointer—there are no members in the struct to access, so there's no way to use this (as far as I can see) to access memory that you shouldn't access. But I'm interested in what the standard says about this, with citations.

Assuming this is fine, is it also fine for any types X and Y such that std::is_empty<X> and std::is_empty<Y> are both true, assuming you don't forget to make an explicit destructor call?


The reason I'm interested in this question is that I have a class like this, which packages up the logic necessary to delay construction of another object:

// A container for an object of type T that can be constructed/destroyed at
// will. This is like std::optional but without the overhead of the presence
// bit.
template <typename T>
class ManuallyConstructed {
 public:
  // Starts uninitialized.
  ManuallyConstructed() = default;
  
  // REQUIRES: not initialized.
  ~ManuallyConstructed() = default;
  
  // Initialize or destroy.
  void Init();
  void Destroy();
  
  // REQUIRES: currently initialized.
  T& operator*();
 
 private:
  // Storage for the object, initialized with placement new by Init.
  alignas(T) char storage_[sizeof(T)];
};

When T is empty according to std::is_empty, I'd like ManuallyConstructed<T> also to be empty. This allows it to benefit from [[no_unique_address]] transparently whenever T would. But it means I need to use an empty type for the storage member, and I still need to be able to provide a T& from the implementation of operator*.


Solution

  • I think this is allowed by the standard, as long as the size and alignment matches. [basic.life]/1 says:

    The lifetime of an object of type T begins when:

    • storage with the proper alignment and size for type T is obtained, and
    • its initialization (if any) is complete (including vacuous initialization) ([dcl.init]),

    [...]

    The lifetime of an object o of type T ends when:

    • [...]
    • the storage which the object occupies is released, or is reused by an object that is not nested within o ([intro.object]).

    This is explicitly acknowledging that storage can be reused for another object. There is nothing in [expr.new] that seems to say the storage can't come from elsewhere. Placement new works by using an allocation function that just returns the input pointer for storage.

    [basic.life]/9 makes it even clearer that we're talking even about types that are not related through inheritance, providing an example (of UB, but not until the end of the block):

    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, and another object of the original type does not occupy that same storage location when the implicit destructor call takes place, the behavior of the program is undefined. This is true even if the block is exited with an exception.

    Example 3:

    class T { };
    struct B {
       ~B();
    };
    
    void h() {
       B b;
       new (&b) T;
    }                               // undefined behavior at block exit
    

    Further, the strict aliasing rule in [basic.lval]/11 is not violated, because to violate it you have to make an access, and an access is defined to involving reading or modifying the value of an object. The definition explicitly notes that that only scalars can be accessed, and there are no scalars here. This is the intuition behind "can't do anything with it" in the original question.

    That said, I believe the strict aliasing rule doesn't apply even with non-empty types, since the original object's lifetime has already ended; there is no sense in which we would be accessing it. In fact, I think I've convinced myself that this pattern is legal in general even for non-empty types, again as long as the size and alignment are correct.