However, I really want behaviour to be logical, and I don't think that swapping internal state for resources like windows during move operations is logical.
A classical implementation of Copy-and-Swap would be like this:
class Image
{
Image();
Image(const Image& copied);
Image(Image&& moved);
void swap(Image& other);
Image& operator= (const Image& copied) {
Image tmp(copied);
tmp.swap(*this);
return *this;
} // object 'tmp' (with previous contents of *this) is destroyed
Image& operator= (Image&& moved)
{
moved.swap(*this);
return *this;
} // object 'moved' (with previous contents of *this) is destroyed
};
Since the temporary object is destroyed after a copy operation, it does not matter what it contains. A moved-from object is
typically destroyed after the move as well, but there are exceptions like
std::unique_ptr which define a behavior. So, unless SFML would explicitly guarantee that a moved-from image is equal to an empty image, we can put any valid content into moved-from objects. Otherwise we would simply involve a default-constructed object to "reset" the image after moving.
The reason why this idiom exists in the first place, is because the naive (or compiler-generated) implementation does this:
Image& operator= (const Image& copied)
{
member0 = copied.member0;
member1 = copied.member1;
// ...
memberN = copied.memberN;
return *this;
}
Image& operator= (Image&& moved)
{
member0 = std::move(moved.member0);
member1 = std::move(moved.member1);
// ...
memberN = std::move(moved.memberN);
return *this;
}
Now imagine that during "...", an exception occurs while copying/moving. The assignment operation is aborted, but both objects are in an inconsistent, "half-moved" or "half-copied" state. This is mostly a problem for copies, as moves should be designed to not fail (however, this is not always possible, e.g. when moves fall back to copies, for raw arrays for example).
What Copy-and-Swap does is -- instead of overwriting the object in-place -- first creating a new valid object, swapping with that and then destroying the old one. Since both swap and destroy operations should not fail, only the construction of the new object can fail, in which case the object is left in the previous, valid state.
So much about the theory. What I mentioned above is absolutely crucial when designing generic building blocks like containers, smart pointers, graphs or other data structures. You don't know what the user will put inside, so you have to assume copies can fail, and design the code accordingly.
Now, in practice, you often have better knowledge about what types are involved, and can exploit this knowledge to simplify code. If SFML resorts to not throwing exceptions during copy/move operations, and has no APIs where user-defined types could trigger exceptions, we can avoid the extra effort and try to use the compiler-generated methods where possible.
Pragmatically, I would attempt to use default move (and even copy) constructors/assignment operators wherever possible -- it's just so much easier to reason about code, to add new fields, etc. But since SFML operates with resources in several places, it involves custom ownership semantics here and there. These are the cases we need to analyze and determine what is the greatest common denominator.