In the following code, if move assignment is uncommented the swap function stops compilation of the program. I observed this behavior on all 3 major compilers (GCC, Clang, MSVC).
#include <utility>
#include <memory>
struct test
{
test() = default;
test(test&& other) noexcept = default;
//test& operator=(test&& other) noexcept = default;
test(const test& other)
: ptr(std::make_unique<int>(*other.ptr))
{}
test& operator=(test other) noexcept
{
std::swap(*this, other);
return *this;
}
std::unique_ptr<int> ptr;
};
Godbolt test: https://godbolt.org/z/v1hGzzEaz
Looking into standard library implementations, they use SFINAE or concepts to enable/disable std::swap overloads and when the special function is uncommented, for some reason, some traits are failing (is_move_constructible and/or is_move_assignable on libstdc++).
My question is: why adding a defaulted special member function prevents the standard library from viewing the type as moveable?
Edit 1: turns out the problem is that xvalues have no preference in overload resolution between T and T&& (which causes ambiguous overload error) so standard library traits fail to recognize the type as moveable.
Edit 2: important: note that the example code IS NOT a proper implementation of the copy and swap idiom. It should be done with custom swap function or use all 5 special member functions. The current implementation creates infinite recursion of move/copy assignment and swap calls.
The main implementation of
std::swapuses move assignment internally and looks something like:This means that move assignment needs to be valid for your type, but it isn't. If you were to call the move assignment operator, you would get an error:
std::swapis constrained so that only MoveAssignable types can be swapped, and yours can't be. Both=operators can be called with an xvalue, and neither is a better match. This is due to the fact that only lvalue-to-rvalue conversion lenghtens the conversion sequence, and initializing an object from an xvalue is considered "free" (as in, this isn't a conversion and not any worse than binding a reference).Even if you could call
std::swap, you cannot simultaneouslystd::swap's default implementation which usesoperator=operator=in terms ofstd::swapThis would be infinite recursion as the definition of
=andstd::swapis circular.Solutions
You can define a custom
swapfor your type so that you no longer rely onstd::swap. Only keepoperator=(test), which would look like:You can also define separate
operator=(const test&)andoperator=(test&&)manually, so thatstd::swapwould use the move assignment operator and there would be no ambiguity in overload resolution.