Ah, I'm talking about swap implemented memberwise, [...]
Usually each data member swap would be inlined, and the compiler would observe that it just needs to swap two chunks of memory, with exceptions for self-referential types.
It's certainly not more efficient than destroy-and-rebuild; and it's more work (at present, you have to write out a swap function by hand). But it is safe against exceptions.
The problem is then, as you pointed out, we don't have anything at the moment to default generate a memberwise swap implementation. I've seen this proposal P0198R0 (
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0198r0.pdf), but I don't know what kind of reception it got.
Ah, thanks for looking that up. I'm not sure I agree with the approach in that paper; I'd go for a one-argument member function, maybe with a tag for std::swap (and even, perhaps, the assignment operators) to select it. But that's by the by.
What about a mixed approach, that could take the best of both worlds? T& operator=(T src) = reloc; would generate the assignment operator with the same aliasing as with the reloc ctor. This bit is important, as it means that the source object will be leaving the assignment operator in a destructed state (and as such its destructor will not be called, as if by the reloc ctor).
Actually, I think that aliasing on relocating assignment is not feasible. The reason is ABI: we can currently write a (non-special) assignment operator with the signature we want to use for relocating assignment, so that ABI cannot change. In particular, we can declare the relocating assignment operator in a header and then later define it out-of-line in a source file, possibly by default: `T& T::operator=(T) = default;`. It would be inelegant if the behavior of that assignment operator were required to be different dependent on whether it was defaulted in-class or out-of-class.
So I think the source object must be destroyed or relocated-from (although this can be elided).
Then I see two possible implementations:
- If T has a noexcept reloc ctor and dtor, then destroy-and-relocate is performed.
- Otherwise, we perform a memberwise destroy-and-relocate by calling the prvalue-assignment operator on each subobject:
- The prvalue-assignment may be user-provided (in which case a temporary copy of the subobject is passed) or generated by =reloc.
- This generated code shall handle exceptions in a way that is similar to constructor initializers: if any subobject prvalue-assignment operator throws, then the destructor of each source subobject that has not been relocated will be called before the exception is propagated. This should be exception safe, as if done by the copy-and-swap idiom.
- The only problem I foresee is if T has a user-provided destructor, in which case the destructor body will not be called on the source object in case an exception is thrown. For that reason, that second implementation should be deleted if T has a user-provided dtor.
Sure, but the user is capable of doing this themselves?
1. T& operator=(T rhs) { static_assert(noexcept(...)); this->~T(); return *new (this) T(reloc rhs); }
2. T& operator=(T) = default; // ill-formed if ~T() is user-provided
Yes, they don't get automatic selection between the two, but I don't really see that as an issue; it'd be quite surprising.