17

I would consider "proper" implementation for is_swappable to be the following:

template<class T, class U = T> struct is_swappable<T, U> : /* see below */ { }

is_swappable inherits from std::true_type if T and U are Swappable, otherwise from std::false_type.


I have tried many things, but SFINAE just doesn't seem to work. This is a particularly nasty counterexample:

struct A {
    A() {}
    ~A() {}
    A(const A&) = delete;
    A(A&&) = delete;
};

Clearly A is not Swappable. Yet any generic solution I can come up with does not properly handle the above example.

A SFINAE implementation I have tried, but did not work looked like this:

namespace with_std_swap {
    using std::swap;

    template<class T, class U, class =
        decltype(swap(std::declval<T&>(), std::declval<U&>()))>
    std::true_type swappable_test(int);

    template<class, class> std::false_type swappable_test(...);
}

template<class T, class U = T>
struct is_swappable
: decltype(with_std_swap::using_std_swap::swappable_test<T, U>(0)) { };

Is there any way to code is_swappable without compiler help?

orlp
  • 112,504
  • 36
  • 218
  • 315
  • start with making `std::false_type swappable_test(...)` a two-parameter template as well – Piotr Skotnicki Nov 04 '14 at 20:56
  • @PiotrS. Woops, that's not why it doesn't work though. Edited in my question and deleted the coliru link. – orlp Nov 04 '14 at 20:57
  • 2
    And you've taken a look at [is_nothrow_swappable](https://github.com/acmorrow/error_or/blob/master/detail/is_nothrow_swappable.hpp) by [acm](https://stackoverflow.com/questions/14483105/why-are-is-swappable-and-is-nothrow-swappable-not-included-in-c11)? –  Nov 04 '14 at 20:59
  • 1
    @remyabel [Yes, it doesn't work for the counterexample.](http://coliru.stacked-crooked.com/a/04347e234cfaf91b) – orlp Nov 04 '14 at 21:01

4 Answers4

4

Building on @jrok's answer, we can tell if an unqualified swap call will call std::swap by writing a swap function with the same signature as std::swap but a unique return type that can then be examined:

namespace detail2 {
    struct tag {};

    template<class T>
    tag swap(T&, T&);

    template<typename T>
    struct would_call_std_swap_impl {

        template<typename U>
        static auto check(int)
        -> std::integral_constant<bool, std::is_same<decltype( swap(std::declval<U&>(), std::declval<U&>())), tag>::value>;

        template<typename>
        static std::false_type check(...);

        using type = decltype(check<T>(0));
    };

    template<typename T>
    struct would_call_std_swap : would_call_std_swap_impl<T>::type { };
}

Then the definition of is_swappable becomes:

template<typename T>
struct is_swappable :
    std::integral_constant<bool,
        detail::can_call_swap<T>::value &&
        (!detail2::would_call_std_swap<T>::value ||
        (std::is_move_assignable<T>::value &&
        std::is_move_constructible<T>::value))
    > { };

We also need a special case for swapping arrays:

template<typename T, std::size_t N>
struct is_swappable<T[N]> : is_swappable<T> {};
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Are all swappable std-types forever guaranteed to support move-assignment and move-construction? – Deduplicator Nov 04 '14 at 21:49
  • 1
    @Deduplicator `std::swap` requires `MoveConstructible` and `MoveAssignable`, so all existing code that uses it should be using it with a type that is both. – T.C. Nov 04 '14 at 21:52
  • 1
    Does that really forbid specializations with reduced requirements? – Deduplicator Nov 04 '14 at 21:53
  • @Deduplicator Amusingly, we can detect if something that calls `std::swap` in the "proper" way is in `namespace std` I think: do a `using std::swap`-less invokation of `swap` on it, and determine if it works (invokes `std::swap`). If it does, it found it via ADL. Which basically gives us a `is_from_std` trait I think? (`would_call_std_swap{} && can_find_swap_via_adt{}`) – Yakk - Adam Nevraumont Nov 04 '14 at 21:55
  • @Deduplicator No, but specializing `std::swap` is already not a very good idea, so I'm not particularly eager to bend over backwards to support it. – T.C. Nov 04 '14 at 21:58
  • 2
    @T.C. Sure. But something WITHIN `std` might specialize `swap`, and do so while not `MoveConstructible` and `MoveAssignable`. It would be `swap` able, but it would fail your test. – Yakk - Adam Nevraumont Nov 04 '14 at 21:58
  • @Yakk Hmm, true. I don't think there's a good way to work around that. – T.C. Nov 04 '14 at 22:01
  • @T.C. see above: we can write `is_from_namespace_std`? I guess that isn't good enough, as you could be within `namespace_std` and not be `swap`able. – Yakk - Adam Nevraumont Nov 04 '14 at 22:03
  • So, it finally resolves to an appeal to the committee to add some SFINAE. – Deduplicator Nov 04 '14 at 22:05
  • @Yakk Right, limited to the homogeneous type case, no invocation of `swap` would result in substitution failure since the base `std::swap` is always available (and any error in the body isn't SFINAE). I suppose the best one can do is to examine all the specializations provided by the standard to see if any of them is on a non-`MoveAssignable` or `MoveConstructible` type, and if so add a specialization. – T.C. Nov 04 '14 at 22:08
  • @TC: As of C++14, that list of special cases is still empty. Still not good. – Deduplicator Nov 04 '14 at 22:17
  • Shouldn't the special case for arrays only apply when `std::swap` is used? – orlp Nov 07 '14 at 01:24
  • isn't it reversed ? shouldn't it be `not std::is_same` since it checks if the return type is `tag` which would mean the type does not get to call std_swap. – v.oddou Mar 06 '15 at 09:24
  • @v.oddou No, the idea is that the `tag`-returning `swap` is a stand-in for `std::swap`; if `std::swap` would have been called outside the `detail2` namespace, it would have called the `tag`-returning `swap` inside. – T.C. Mar 06 '15 at 14:06
2

Here's my take on this:

#include <iostream>
#include <type_traits>

#include <utility>
namespace detail {
    using std::swap;

    template<typename T>
    struct can_call_swap_impl {

        template<typename U>
        static auto check(int)
        -> decltype( swap(std::declval<T&>(), std::declval<T&>()),
                std::true_type());

        template<typename>
        static std::false_type check(...);

        using type = decltype(check<T>(0));
    };

    template<typename T>
    struct can_call_swap : can_call_swap_impl<T>::type { };
}

template<typename T>
struct is_swappable :
    std::integral_constant<bool,
        detail::can_call_swap<T>::value &&
        std::is_move_assignable<T>::value &&
        std::is_move_constructible<T>::value
    > { };

struct A
{
    A() {}
    ~A() {}
    A(const A&) = delete;
    A(A&&) = delete;
};

int main()
{
    std::cout << is_swappable<A>{};
}

The reason yours doesn't work is that it only checks whether it is ok to call swap, not whether it would actualy compile if it were instantiated. That's out of realm of SFINAE (not immediate context).

So I just extended the test with the requirements for std::swap, that is - T must be MoveAssignable and MoveConstructible.

jrok
  • 54,456
  • 9
  • 109
  • 141
  • 1
    `std::swap` does perform move-assignment and move-construction, but I can imagine user-defined swap that simply swaps internal pointers, so I would suggest checking `is_move_assignable` and `is_move_constructible` only for `std::swap`, and just `can_call_non_std_swap` for other cases. – Piotr Skotnicki Nov 04 '14 at 21:03
  • @PiotrS. Good point, but do you have an idea how to reliably tell `std::swap` and other swap functions apart? – jrok Nov 04 '14 at 21:06
  • @orlp Yeah, that's what I was afraid of, let's see if there's a workaround :) – jrok Nov 04 '14 at 21:07
2

After much thought, the ideas posted by the other answers, and finding defects in the C++ standard I think I've got the solution that is as close as you can get to a compile-time check for the Swappable concept.

It's not pretty. It uses a trick to detect if std::swap is used by providing a function with the exact same signature as proposed by T.C.. Then we write helper functions to detect if swapping is possible at all, and whether it resolves to std::swap. The last helper templates are used to see if std::swap will be noexcept. This does not use the exact semantics as put forth in the C++14 standard, and assumes the what I think to be intended behaviour of swapping multidimensional arrays being noexcept.

namespace detail {
    namespace swap_adl_tests {
        // if swap ADL finds this then it would call std::swap otherwise (same signature)
        struct tag {};

        template<class T> tag swap(T&, T&);
        template<class T, std::size_t N> tag swap(T (&a)[N], T (&b)[N]);

        // helper functions to test if an unqualified swap is possible, and if it becomes std::swap
        template<class, class> std::false_type can_swap(...) noexcept(false);
        template<class T, class U, class = decltype(swap(std::declval<T&>(), std::declval<U&>()))>
        std::true_type can_swap(int) noexcept(
            noexcept(swap(std::declval<T&>(), std::declval<U&>()))
        );

        template<class, class> std::false_type uses_std(...);
        template<class T, class U>
        std::is_same<decltype(swap(std::declval<T&>(), std::declval<U&>())), tag> uses_std(int);

        template<class T>
        struct is_std_swap_noexcept : std::integral_constant<bool,
            std::is_nothrow_move_constructible<T>::value &&
            std::is_nothrow_move_assignable<T>::value
        > { };

        template<class T, std::size_t N>
        struct is_std_swap_noexcept<T[N]> : is_std_swap_noexcept<T> { };

        template<class T, class U>
        struct is_adl_swap_noexcept : std::integral_constant<bool, noexcept(can_swap<T, U>(0))> { };
    }
}

template<class T, class U = T>
struct is_swappable : std::integral_constant<bool, 
    decltype(detail::swap_adl_tests::can_swap<T, U>(0))::value &&
        (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value ||
            (std::is_move_assignable<T>::value && std::is_move_constructible<T>::value))
> {};

template<class T, std::size_t N>
struct is_swappable<T[N], T[N]> : std::integral_constant<bool, 
    decltype(detail::swap_adl_tests::can_swap<T[N], T[N]>(0))::value &&
        (!decltype(detail::swap_adl_tests::uses_std<T[N], T[N]>(0))::value ||
            is_swappable<T, T>::value)
> {};

template<class T, class U = T>
struct is_nothrow_swappable : std::integral_constant<bool, 
    is_swappable<T, U>::value && (
        (decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
            detail::swap_adl_tests::is_std_swap_noexcept<T>::value)
        ||
        (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
            detail::swap_adl_tests::is_adl_swap_noexcept<T, U>::value)
    )
> {};
Community
  • 1
  • 1
orlp
  • 112,504
  • 36
  • 218
  • 315
  • 1
    Fails to compile for me in Visual Studio 2015 with SP3: error C2672: 'beast::detail::swap_adl_tests::swap': no matching overloaded function found – Vinnie Falco Oct 03 '16 at 14:14
0

How about:

#if __cplusplus < 201703L
template<typename...> using void_t = void;
#else
using std::void_t;
#endif

template <typename T, typename = void>
struct is_swappable : std::false_type {};
                      
template <typename T>
struct is_swappable<T, void_t<decltype(swap(std::declval<T&>(), std::declval<T&>()))>> : std::true_type {};

EDIT:

namespace swap_details
{
    #if __cplusplus < 201703L
    template<typename...> using void_t = void;
    #else
    using std::void_t;
    #endif

    using std::swap;
    
    template <typename T, typename = void>
    struct is_swappable : std::false_type {};

    template <typename T>
    struct is_swappable<T, void_t<decltype(swap(std::declval<T&>(), std::declval<T&>()))>> : std::true_type {};
    
}

template <typename T>
struct is_swappable : public swap_details::is_swappable<T>
{
};