Search code examples
c++c++17swaptype-traitsstd

std::is_swappable_v is false on a swappable class template


I'm trying to write a swappable class template, S<T>. I'd like S<T> and S<T&> to be swappable if T is swappable (e.g. S<int> should be swappable with S<int&>). So I went for something like this:

#include <type_traits>

namespace n {

template <typename T>
struct S {};

// Overload #1
template <typename T, typename U>
void swap(S<T>&, S<U>&) {}

// Overload #2
// template <typename T>
// void swap(S<T>&, S<T>&) {}

}  // namespace n

int main() {
    n::S<int> sInt;
    swap(sInt, sInt);
    // static_assert(std::is_swappable_v<decltype(sInt)>);
    //               ^ assertion fails unless I uncomment #2

    n::S<int&> sIntRef;
    swap(sInt, sIntRef);
    static_assert(
        std::is_swappable_with_v<decltype(sInt)&, decltype(sIntRef)&>);
    //  ^ assertion evaluates the way I expect
}

As you can see above, is_swappable_v<n::S<int>> is false even though the call to swap(sInt, sInt) compiles. In order for is_swappable_v<n::S<int>> to evaluate to true, I need to uncomment overload #2 for swap.

Am I doing anything wrong or is type trait std::is_swappable actually supposed to work this way?


Solution

  • The expression swap(sInt, sInt) doesn't actually perform a swap because std::swap isn't visible (through using std::swap;).

    When you do have it:

    int main() {
        n::S<int> sInt;
    
        using std::swap;
        swap(sInt, sInt);
    }
    

    it doesn't compile, so your type isn't actually swappable with itself.

    The reason for this is that there are two possible overloads:

    template <typename T, typename U>
    void n::swap(S<T>&, S<U>&);
    // with T = int, S = int
    
    template <typename T>
    void std::swap(T&, T&);
    // with T = n::S<int>
    

    The first one would be better because each argument is more specialized.
    The second one would be better because both arguments are the same and they come from the same template argument.

    Since they are both better in different ways, it makes it an ambiguous call.

    When you add your second template overload:

    template <typename T>
    void n::swap(S<T>&, S<T>&);
    // with T = int
    

    This is better than the first n::swap because of the repeated argument, and better than std::swap because it's more specialized, so it is chosen unambiguously.