I wonder whether we can use a union
member as storage for an explicitly initialized and destructed object, such as in the following code:
struct X
{
X() { std::cout << "C"; }
~X() { std::cout << "D"; }
};
struct X_owner
{
union
{
X x; // storage for owned object
};
X_owner()
{
new (&x) X{};
}
~X_owner()
{
(&x)->~X();
}
};
int main()
{
X_owner xo;
}
The printed output is as expected only CD
. Live demo: https://godbolt.org/z/M1Gov4o4d
The question is motivated by a programming assignment, where students were supposed to explicitly define storage for an object. The expected solution was a suitably aligned and sized buffer of type unsigned char
or std::byte
, or std::aligned_storege
. However, few of them used union
this way within their solutions. And I am not sure whether this is correct, though I cannot find anything wrong about it.
Note: For the sake of simplicity, I do not care about copy/move semantics in this example.
CodePudding user response:
It is indeed valid.
[class.base.init]
9 In a non-delegating constructor, if a given potentially constructed subobject is not designated by a mem-initializer-id (including the case where there is no mem-initializer-list because the constructor has no ctor-initializer), then
- [...]
- otherwise, if the entity is an anonymous union or a variant member ([class.union.anon]), no initialization is performed;
- [...]
So x
is not initialized, and so is not within its lifetime until one explicitly does something with that lump of storage to create an object there. The placement new isn't even replacing an object in that storage, there was non there until the new expression. So we aren't even delving into places where std::launder
may be required.
Seems like an acceptable approach for doing it.
CodePudding user response:
The shown code is correct. To be functionally complete, a copy constructor and an assignment operator should be formally defined. Things become complicated, very quickly, if the union
were to hold another object. To remain valid the wrapper object will need to explicitly track which underlying object is constructed, in the union, and handle these matters by itself.
But, if things were to go in that direction you can just let std::variant
deal with it.