Reputation: 5676
I have a struct containing an object of some type T
, which I know to have a noexcept default constructor. I would like to expose a member function on my struct that replaces the old object with a new object. However, T
might not have an assignment operator, so I would need to destroy the old T
and construct a new T
into the same variable.
Thus, I would like to write code like this:
template <typename T>
class Container {
private:
T t{};
public:
template <typename... Args>
void replace(Args&&... args) {
// destroy the old T...
std::destroy_at(&t);
// ... and construct a new T in its place (or if it throws, we default-construct a T)
try {
std::construct_at(&t, std::forward<Args>(args)...);
} catch {
std::construct_at(&t);
}
}
// stuff that we might do on the object, both before and after the call to replace()
void do_stuff() { t.do_stuff(); }
};
Is the above valid code, and is it possible to do things with the object before and after the replace operation without invoking undefined behaviour? (Or perhaps it is undefined behaviour that could be made well-defined with a sprinkling of std::launder
?)
I don't think this part is necessary to answer the question, but in the specific problem I'm trying to solve, T
has reference-like semantics, and therefore assignment is akin to rebinding the reference, which T
may not allow.
Note that this is a similar question, but the answers it has garnered only say one or more of the following:
T
throws.I am however interested mainly in whether my code is well-defined according to the standard, and none of the answers there sufficiently address this. In addition, the linked question was asked before it was possible to implement std::vector
in standard C++.
Upvotes: 2
Views: 129
Reputation: 29032
This is probably fine, depending on what exactly T
is and how Container
is used. T
and Container
must be transparently replaceable.
In C++20, the relevant passage is 6.7.3.8 in [basic.life] :
- 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 original object is neither a complete object that is const-qualified nor a subobject of such an object, and
- (8.4) neither the original object nor the new object is a potentially-overlapping subobject (6.7.2)
- (8.5) either the original object and the new object are both complete objects, or the original object and the new object are direct subobjects of objects p1 and p2, respectively, and p1 is transparently replaceable by p2.
In your case you are asking if the name of the original object t
will refer to the new object, which it will if the requirements above are met.
8.1 and 8.2 are met because you are creating a new object at the exact same location with the exact same type.
8.3 depends on T and this
. If neither are const
then this requirement is also met.
8.4 is met because t
and the newly created object are not potentially overlapping objects. 6.7.2.7 defines potentially overlapping object as a base class subobject or a non-static data member declared with the no_unique_address attribute.
8.5 is met since both objects are subobjects of the Container
which is presumed to also be transparently replaceable. If the case where Container
is not transparently replaceable, such as if it's is const
or a base class subobject, then t
is also not transparently replaceable, and the replace
function couldn't be called. Making the Container
class final
may help avoid this trap.
Upvotes: 2