Your game of "construct a new object in place of the old one" is the problem.
- It is completely forbidden if the object is
const or contains any const member subobjects.
due to the following rule in [basic.life] (note that a rewrite1 of this rule is proposed in post-C++17 drafts)
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
- 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).
You have to abide by this rule for the purposes both of return *this; and also the implicit destructor call.
- It also doesn't work during
constexpr evaluation.
... this one is specific to the fact that std::string small-string optimization may be implemented using a union, and changing an active union member has been forbidden during constexpr evaluation, although this rule too seems to have changed post-C++17.
1 I consider said change to be misguided (it doesn't even permit the pattern it was supposed to fix) and break legitimate coding patterns. While it's true that a pointer to const-qualified object only made my view readonly and did not let me assume that the object wasn't being changed by someone else holding a pointer/reference that wasn't so qualified, in the past if I was given a pointer (qualified or not) to an object with a const member, I was assured that no one was changing that member and I (or my optimizing compiler) could safely use a cached copy of that member (or data derived from that member value, such as a hash or a comparison result).
Apparently this is no longer true.
While changing the language rule may automatically remove all compiler optimizations that would have assumed immutability of a const member, there's no automatic patch to user-written code which was correct and bug-free under the old rules, for example std::map and std::unordered_map code using std::pair<const Key, Value>. Yet the DR doesn't appear to have considered this as a breaking change...
I was asked for a code snippet that illustrates a behavior change of existing valid code, here it is. This code was formerly illegal, under the new rules it's legal, and the map will fail to maintain its invariants.
std::map<int, T> m{data_source()};
/* new code, now legal */
for( auto& keyvalue : m ) {
int newkey = -keyvalue.first;
std::construct_at(&keyvalue.first, newkey);
// or new (&keyvalue.first) int(newkey);
}
/* existing valid code that breaks */
std::cout << m[some_key()];
Consider the new relaxed wording of the restriction
the original object is neither a complete object that is const-qualified nor a subobject of such an object
keyvalue.first is const-qualified, but it is not a complete object, and it is a subobject of a complete object (std::pair<const Key, Value>) that is not const-qualified. This code is now legal. It's not even against the spirit of the rule, the DR explicitly mentioned the intent to perform in-place replacement of container elements with const subobjects.
It's the implementation of std::map that breaks, along with all existing code that uses the map instance, an unfortunate action-at-a-distance resulting from addition of now-legal code.
Please note that the actual replacement of the key could take place in code that merely has the pointer &keyvalue and needn't know that std::pair instance is actually inside a std::map), so the stupidity of what's being done won't be so obvious.