Document number: N3619
Date: 2013-03-15
Project: Programming Language C++, Library Working Group
Reply-To: Andrew Morrow <[email protected]>

A proposal to add swappability traits to the standard library

I. Table of Contents

  1. Introduction
  2. Motivation and Scope
  3. Impact on the Standard
  4. Design Decisions
  5. Technical Specification
  6. Implementation
  7. Acknowledgements
  8. References

II. Introduction

This proposal proposes the addition of two new type property predicates to the C++ standard library: std::is_swappable<T, U = T> and std::is_nothrow_swappable<T, U = T>.

The type property predicate std::is_swappable<T, U = T> will derive from std::true_type if the expressions 'swap(declval<T>(), declval<U>())' and 'swap(declval<T>(), declval<U>())' are both well-formed when std::swap is in scope, and from std::false_type otherwise.

The type property predicate std::is_nothrow_swappable<T, U = T> will derive from std::true_type if std::is_swappable<T, U> derives from std::true_type and the expression 'swap(declval<T>(), declval<U>()) is known to be non-throwing, and from std::false_type otherwise.

III. Motivation and Scope

When writing a template it is often important to know whether or not values of a particular template type parameter are swappable, either via std::swap or by a swap overload found by argument dependent lookup ("ADL swap"). This information could be used in combination with std::enable_if, for instance, to inhibit generation of a swap overload for the template for the case where values for a particular type parameter are not swappable. It is also often important to know whether a call to 'swap' may throw, primarily to propagate noexcept. In particular, the noexcept status of a swap call is of interest when implementing a copy assignment operator or move assignment operator in terms of swap to achieve the strong exception safety guarantee.

The current set of type property predicates defined by the standard does not include traits to determine whether a swap is available for any two types, or, if it is, whether or not the resolved swap is non-throwing. Without these traits, it is left to library authors to use the existing library and language features to make the determination. However, the 'obvious' solutions are often wrong: For example, in the case of attempting to determine the noexcept status of a function that calls 'swap', it is not possible to form a correct noexcept specifier directly:

We can't use std::swap inside the noexcept operator if we are using ADL swap in the body:


template<typename T>
struct X {
    // The swap in the noexcept expression may not be the same swap as found by ADL below.
    friend void swap(X& a, X& b) noexcept(noexcept(std::swap(std::declval<T&>(), std::declval<T&>()))) {
        using std::swap;
        swap(a.val, b.val);
    }
    T val;
};
    

But we can't use a non-qualified swap in the noexcept operator either:


template<typename T>
struct X {
    // ERROR if std::swap is not in scope and there is no swap found by ADL for T.
    friend void swap(X& a, X& b) noexcept(noexcept(swap(std::declval<T&>(), std::declval<T&>()))) {
        using std::swap;
        swap(a.val, b.val);
    }
    T val;
};
    

And we can't enable ADL swap inside the noexcept operator:


template<typename T>
struct X {
    // ERROR: Cannot have a statement inside a noexcept operator; it requires an expression.
    friend void swap(X& a, X& b) noexcept(noexcept(using std::swap; swap(std::declval<T&>(), std::declval<T&>()))) {
        using std::swap;
        swap(a.val, b.val);
    }
    T val;
};
    

While it is not particularly difficult to build workarounds for this issue [1][2] (and in particular see libc++[3] which defines and makes extensive use of these traits within its implementation, but of course does not expose them), it seems reasonable that a future version of the standard library should provide this facility. This is especially true given the fundamental importance of swap (and of non-throwing swap in particular) to properly implementing move operations.

IV. Impact On the Standard

When is_swappable and is_nothrow_swappable were first proposed, it was expected that they both could be implemented in C++11 as pure library extensions. However, the following example demonstrates that this is not true:


#include <swap_traits>

struct X {
    X& operator=(X const&) = delete;
};

static_assert(!is_swappable<X>::value_type);
    

This code unfortunately will compile with the partial implementation of is_swappable proposed below, even though it should not, because in an unevaluated context the call to std::swap is OK, but instantiation would fail due to X not meeting the requirements for std::swap.

There are a few possible solutions:

V. Design Decisions

Proposed Design

Goals

The behavior of the proposed classes is designed to be as similar possible to the existing type property classes defined in the <type_traits> header and to meet the requirements of [20.9.4] for templates that may be used to query the properties of a type at compile time. Specifically, the classes meet the requirements for a UnaryTypeTrait as described in [20.9.1]. The names of the proposed classes are selected to be as similar as possible to the names of the existing type property classes as described in [20.9.4].

Alternative Designs

One potential way out of the difficulty of obtaining the right swap in the noexcept operator would be if the language provided a "using expression" rather than a "using statement", in which case the third example above could be made to work:


template<typename T>
struct X {
    // OK?: Some sort of way to have an expression that 'uses' something.
    friend void swap(X& a, X& b) noexcept(noexcept(with using std::swap swap(std::declval<T&>(), std::declval<T&>()))) {
        using std::swap;
        swap(a.val, b.val);
    }
    T val;
};
      

One advantage of having such a 'using expression' available in the language is that it solves the more general problem of obtaining names via 'std or ADL' inside a noexcept operator. However, this would be a much more significant change, involving updates to the core language rather than just the standard library, and it is not clear how often such a feature would be needed for things other than swap.

VI. Technical Specifications

Header

The new classes will be added to a new header <swap_traits>

20.9.X Header <swap_traits> synopsis

  1. The header <swap_traits> defines the type property predicates is_swappable and is_nothrow_swappable. The <swap_traits> header includes <utility> to ensure that std::swap is among the candidate functions considered by these predicates, as required by 20.9.4.3.

#include <utility>

namespace std {
  template<typename T, typename U = T>
  struct is_swappable;

  template<typename T, typename U = T>
  struct is_nothrow_swappable;
}
    

20.9.4.3 Type Properties [meta.unary.prop]

Add two new rows to Table 49:
Table 49 - Type property predicates
TemplateConditionPreconditions

template<typename T, typename U = T>
struct is_swappable;
            
The expressions swap(declval<T>(), declval<U>()) and swap(declval<U>(), declval<T>()) are each well-formed when treated as an unevaluated operand (Clause 5). The context in which the aforementioned expressions are considered shall ensure that a candidate set for swap consists of the two swap function templates defined in <utility> (20.2) and the lookup set produced by argument-dependent lookup (3.4.2).

template<typename T, typename U = T>
struct is_nothrow_swappable;
            
is_swappable<T,U>::value is true, and the expression swap(declval<T>(), declval<U>()) is known not to throw any exceptions (5.3.7). The context in which the aforementioned swap expression is considered is the same as that for is_swappable.

VII. Partial Implementation

The following is partial implementation of the above specification, derived from [2], which was itself heavily influenced by the existing implementation in libc++[3] and discussions on stackoverflow [1][4].

As noted above, this implementation is incomplete.


namespace std {

template<typename __T, typename __U>
class __is_swappable_test {

    struct __swap_not_found_type {};

    template<typename __V1, typename __V2>
    static auto __test(__V1&& __v1, __V2&& __v2) -> decltype(swap(std::forward<__V1>(__v1), std::forward<__V2>(__v2)));

    template<typename __V1, typename __V2>
    static auto __test(...) -> __swap_not_found_type;

    using __test_type_tu = decltype(__test<__T, __U>(std::declval<__T>(), std::declval<__U>()));
    using __test_type_ut = decltype(__test<__U, __T>(std::declval<__U>(), std::declval<__T>()));

public:
    static constexpr bool __value =
        !std::is_same<__test_type_tu, __swap_not_found_type>::value &&
        !std::is_same<__test_type_ut, __swap_not_found_type>::value;
};

template<bool, typename __T, typename __U>
struct __is_nothrow_swappable_test :
    std::conditional<
    noexcept(swap(std::declval<__T>(), std::declval<__U>())),
    std::true_type, std::false_type>::type {};

template<typename __T, typename __U>
struct __is_nothrow_swappable_test<false, __T, __U> :
    std::false_type {};

template<typename __T, typename __U = __T>
struct is_swappable :
    std::conditional<__is_swappable_test<__T, __U>::__value,
                     std::true_type, std::false_type>::type {};

template<typename __T, typename __U = __T>
struct is_nothrow_swappable :
    __is_nothrow_swappable_test<is_swappable<__T, __U>::value, __T, __U> {};

} // namespace std
    

VIII. Acknowledgements

IX. References