Document number: N4511
Date: 2015-05-22
Revises: N4426
Author: Daniel Krügler
Project: Programming Language C++, Library Working Group
Reply-to: Daniel Krügler

Adding [nothrow-]swappable traits, revision 1

Revision History

Changes since N4426:

Discussion

This proposal suggests to add new type traits std::is_swappable<T>, std::is_swappable_with<T, U>, std::is_nothrow_swappable<T>, and std::is_nothrow_swappable_with<T, U> to the header <type_traits> to resolve the existing library issue LWG 2456 involving broken noexcept-specifications of several member swap functions of a number of library components. In addition to that, this paper suggests to specify the two swap templates from header <utility> as constrained templates.

This paper revision does not repeat contents of it's predecessor N4426. Please refer to the original paper regarding general background and rationale.

Design Rationale Extension

During the Lenexa meeting concerns were expressed that the trait test

is_swappable<std::string&&>::value

will evaluate to true and this could suggest to the user that rvalues of std::string could be swapped.

So, is that a desired result for this trait?

Responding to the last question first: Surprisingly, the answer to that question is "Yes!" — It is indeed a desired outcome of this trait query, which manifests when we reflect upon the effects this trait test result has on std::tuple's member swap. The tuple template is especially noteworthy in this context, because it is the only library component that pro-actively supports members that are rvalue references via the factory function forward_as_tuple:

template <class... Types>
  constexpr tuple<Types&&...> forward_as_tuple(Types&&...) noexcept;

Let's assume for a moment that the above mentioned trait would evaluate to false. This would essentially mean that we would say that the type std::tuple<std::string&&> would not be lvalue-swappable. This is in contradiction to the well-defined characteristics of

void swap(tuple& rhs)

which is specified as follows:

-2- Requires: Each element in *this shall be swappable with (17.6.3.2) the corresponding element in rhs.

-3- Effects: Calls swap for each element in *this and its corresponding element in rhs.

The wording essentially describes that two lvalues which are declared as std::string&& are swapped. Conceptually this can be written as

std::string&& a = ...;
std::string&& b = ...;
using std::swap;
swap(a, b);

The above example presents more clearly that we are not talking about an attempt of swapping rvalues, but we are still talking about swapping of lvalues.

In other words: A possible pictorial way of describing what is_swappable<T&&>::value is asking for a given object type T is as follows:

Two lvalues that are rvalue-references of type T are swappable.

Looking at this phrase a second time it turns out that the part "that are rvalue-references" is completely non-essential and can be removed giving us the usual way of saying it as

Two lvalues of type T are swappable.

This "word-collapsing" corresponds to the same mechanics as the well-known "reference-collapsing" rule that is the foundation of the specification of the trait is_swappable in the proposed wording below, when T corresponds to any sort of reference type.

Is this astonishing characteristics of the is_swappable specification unique among the other traits and should therefore be considered as hard-to-teach?

The author of this paper considers this case as not unique. The sometimes surprising consequences of reference-collapsing and "hidden" reference additions in the specification of type-traits is a known gotcha. Two examples are the observation that is_assignable<int, int>::value returns false ("But I can assign two int's, right?") and that is_move_assignable<std::string&>::value returns true, albeit according to common sense we cannot move-assign from an lvalue.

Resolved Issues

If the proposed resolution would be accepted, the following library issues will be resolved:

Number Description
2456 Incorrect exception specifications for 'swap' throughout library

Related Issues

The current C++ Extensions for Library Fundamentals is affected by the very same problem as highlighted by LWG 2456. The specification of std::experimental::optional's member swap in 5.3.4 [optional.object.swap] is:

void swap(optional<T>& rhs) noexcept(see below);

with:

Remarks: The expression inside noexcept is equivalent to:

is_nothrow_move_constructible_v<T> && noexcept(swap(declval<T&>(), declval<T&>()))

The author of this paper is intending to open a new library issue for this problem and to prepare a future proposal that suggests the following changes to the C++ Extensions for Library Fundamentals:

  1. To add the following new variable templates to header <experimental/type_traits>:

    template <class T, class U> constexpr bool is_swappable_with_v =
      is_swappable_with<T, U>::value;
    template <class T> constexpr bool is_swappable_v =
      is_swappable<T>::value;
    template <class T, class U> constexpr bool is_nothrow_swappable_with_v =
      is_nothrow_swappable_with<T, U>::value;
    template <class T> constexpr bool is_nothrow_swappable_v =
      is_nothrow_swappable<T>::value;
    
  2. To change 5.3.4 [optional.object.swap] p.5:

  3. Remarks: The expression inside noexcept is equivalent to:

    is_nothrow_move_constructible_v<T> && is_nothrow_swappable_v<T>noexcept(swap(declval<T&>(), declval<T&>()))
    

The above presented change suggestions are not part of this proposal!

Proposed Resolution

At some places below, additional markup of the form

[Drafting notes: whatever — end drafting notes]

is provided, which is not part of the normative wording, but is solely shown to provide additional information to the reader about the rationale of the concrete wording.

The proposed wording changes refers to N4431.

  1. Change header <utility> synopsis, 20.2 [utility] p2, as indicated:

    […]
    // 20.2.2, swap:
    template<class T> void swap(T& a, T& b) noexcept(see below);
    template <class T, size_t N> void swap(T (&a)[N], T (&b)[N]) noexcept(is_nothrow_swappable<T>::valuenoexcept(swap(*a, *b)));
    […]
    
  2. Change 20.2.2 [utility.swap] as indicated:

    [Drafting notes:

    1. The Requires elements have been left intentionally, based on the assumption that the additional semantic constraints implied by the MoveConstructible and MoveAssignable are important to keep.

    2. The replacement of the expression noexcept(swap(*a, *b)) within the noexcept-specification is not a required change, but seems to improve the symmetry between exception-specifications and template constraints and is therefore recommended by the author. In addition, a similar approach is already necessary in existing implementation headers when forward declarations of the swap templates are provided.

    3. The seemingly recursive relation between the is_[nothrow_]swappable trait definitions and especially of the second swap declaration is resolvable for a concrete implementation by a suitable sequence of (non-defining) declarations.

    end drafting notes]

    template<class T> void swap(T& a, T& b) noexcept(see below);
    

    -1- Remarks: This function shall not participate in overload resolution unless is_move_constructible<T>::value is true and is_move_assignable<T>::value is true. The expression inside noexcept is equivalent to:

    is_nothrow_move_constructible<T>::value &&
    is_nothrow_move_assignable<T>::value
    

    -2- Requires: Type T shall be MoveConstructible (Table 20) and MoveAssignable (Table 22).

    -3- Effects: […]

    template<class T, size_t N>
      void swap(T (&a)[N], T (&b)[N]) noexcept(is_nothrow_swappable<T>::valuenoexcept(swap(*a, *b)));
    

    -?- Remarks: This function shall not participate in overload resolution unless is_swappable<T>::value is true.

    -4- Requires: a[i] shall be swappable with (17.6.3.2) b[i] for all i in the range [0, N).

    -5- Effects: swap_ranges(a, a + N, b)

  3. Change 20.3.2 [pairs.pair] as indicated:

    void swap(pair& p) noexcept(see below);
    

    -31- Remarks: The expression inside noexcept is equivalent to:

    is_nothrow_swappable<first_type>::valuenoexcept(swap(first, p.first)) &&
    is_nothrow_swappable<second_type>::valuenoexcept(swap(second, p.second))
    

    […]

  4. Change 20.4.2.3 [tuple.swap] as indicated:

    void swap(tuple& rhs) noexcept(see below);
    

    -1- Remarks: The expression inside noexcept is equivalent to the logical and of the following expressions:

    is_nothrow_swappable<Ti>::valuenoexcept(swap(declval<Ti&>>(), declval<Ti&>()))
    

    where Ti is the ith type in Types.

  5. Change 20.10.2 [meta.type.synop], header <type_traits> synopsis, as indicated:

    namespace std {
      […]
      // 20.10.4.3, type properties:
      […]
      template <class T> struct is_move_assignable;
      
      template <class T, class U> struct is_swappable_with;
      template <class T> struct is_swappable;
      
      template <class T> struct is_destructible;
      […]
      template <class T> struct is_nothrow_move_assignable;
    
      template <class T, class U> struct is_nothrow_swappable_with;
      template <class T> struct is_nothrow_swappable;
      
      template <class T> struct is_nothrow_destructible;
      […]
    }
    
  6. Change 20.10.4.3 [meta.unary.prop], Table 49 — "Type property predicates", as indicated:

    [Drafting notes:

    1. The term referenceable type, referred to below, is defined in 17.3.19 [defns.referenceable].

    2. The specification below allows, but does not require, that an implementation defines the traits is_swappable and is_nothrow_swappable, respectively, in terms of the implementation details of the more general traits is_swappable_with and is_nothrow_swappable_with, respectively.

    end drafting notes]

    Table 49 — Type property predicates
    Template Condition Preconditions
    template <class T, class U>
    struct is_swappable_with;
    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) in an overload-resolution
    context for swappable values (17.6.3.2 [swappable.requirements]). Access
    checking is performed as if in a context unrelated to T and U. Only the
    validity of the immediate context of the swap expressions is considered.
    [Note: The compilation of the expressions can result in side effects such
    as the instantiation of class template specializations and function template
    specializations, the generation of implicitly-defined functions, and so on. Such
    side effects are not in the "immediate context" and can result in the program
    being ill-formed. — end note]
    T and U shall be complete types,
    (possibly cv-qualified) void, or
    arrays of unknown bound.
    template <class T>
    struct is_swappable;
    For a referenceable type T, the same result
    as is_swappable_with<T&, T&>::value,
    otherwise false.
    T shall be a complete type,
    (possibly cv-qualified) void, or an
    array of unknown bound.
    template <class T, class U>
    struct is_nothrow_swappable_with;
    is_swappable_with<T, U>::value is true
    and each swap expression of the definition of
    is_swappable_with<T, U> is known not to throw
    any exceptions (5.3.7 [expr.unary.noexcept]).
    T and U shall be complete types,
    (possibly cv-qualified) void, or
    arrays of unknown bound.
    template <class T>
    struct is_nothrow_swappable;
    For a referenceable type T, the same result
    as is_nothrow_swappable_with<T&, T&>::value,
    otherwise false.
    T shall be a complete type,
    (possibly cv-qualified) void, or an
    array of unknown bound.
  7. Change 23.3.2.1 [array.overview] p3, class template array overview, as indicated:

    namespace std {
      template <class T, size_t N>
      struct array 
      {
        […]
        void swap(array&) noexcept(is_nothrow_swappable<T>::valuenoexcept(swap(declval<T&>(), declval<T&>())));
        […]
      };
    }
    
  8. Change 23.3.2.7 [array.swap] before p1 as indicated:

    void swap(array& y) noexcept(is_nothrow_swappable<T>::valuenoexcept(swap(declval<T&>(), declval<T&>())));
    
  9. Change 23.4.4.1 [map.overview] p2, class template map overview, as indicated:

    namespace std {
      template <class Key, class T, class Compare = less<Key>,
                class Allocator = allocator<pair<const Key, T> > >
      class map 
      {
        […]
        void swap(map&)
          noexcept(allocator_traits<Allocator>::is_always_equal::value &&
                   is_nothrow_swappable<Compare>::valuenoexcept(swap(declval<Compare&>(),declval<Compare&>())));
        […]
      };
    }
    
  10. Change 23.4.5.1 [multimap.overview] p2, class template multimap overview, as indicated:

    namespace std {
      template <class Key, class T, class Compare = less<Key>,
                class Allocator = allocator<pair<const Key, T> > >
      class multimap 
      {
        […]
        void swap(multimap&)
          noexcept(allocator_traits<Allocator>::is_always_equal::value &&
                   is_nothrow_swappable<Compare>::valuenoexcept(swap(declval<Compare&>(),declval<Compare&>())));
        […]
      };
    }
    
  11. Change 23.4.6.1 [set.overview] p2, class template set overview, as indicated:

    namespace std {
      template <class Key, class Compare = less<Key>,
                class Allocator = allocator<Key> >
      class set 
      {
        […]
        void swap(set&)
          noexcept(allocator_traits<Allocator>::is_always_equal::value &&
                   is_nothrow_swappable<Compare>::valuenoexcept(swap(declval<Compare&>(),declval<Compare&>())));
        […]
      };
    }
    
  12. Change 23.4.7.1 [multiset.overview] p2, class template multiset overview, as indicated:

    namespace std {
      template <class Key, class Compare = less<Key>,
                class Allocator = allocator<Key> >
      class multiset 
      {
        […]
        void swap(multiset&)
          noexcept(allocator_traits<Allocator>::is_always_equal::value &&
                   is_nothrow_swappable<Compare>::valuenoexcept(swap(declval<Compare&>(),declval<Compare&>())));
        […]
      };
    }
    
  13. Change 23.5.4.1 [unord.map.overview] p3, class template unordered_map overview, as indicated:

    namespace std {
      template <class Key, 
                class T,
                class Hash = hash<Key>,
                class Pred = std::equal_to<Key>,
                class Allocator = std::allocator<std::pair<const Key, T> > >
      class unordered_map 
      {
        […]
        void swap(unordered_map&)
          noexcept(allocator_traits<Allocator>::is_always_equal::value &&
                   is_nothrow_swappable<Hash>::valuenoexcept(swap(declval<Hash&>(),declval<Hash&>())) &&
                   is_nothrow_swappable<Pred>::valuenoexcept(swap(declval<Pred&>(),declval<Pred&>())));
        […]
      };
    }
    
  14. Change 23.5.5.1 [unord.multimap.overview] p3, class template unordered_multimap overview, as indicated:

    namespace std {
      template <class Key, 
                class T,
                class Hash = hash<Key>,
                class Pred = std::equal_to<Key>,
                class Allocator = std::allocator<std::pair<const Key, T> > >
      class unordered_multimap 
      {
        […]
        void swap(unordered_multimap&)
          noexcept(allocator_traits<Allocator>::is_always_equal::value &&
                   is_nothrow_swappable<Hash>::valuenoexcept(swap(declval<Hash&>(),declval<Hash&>())) &&
                   is_nothrow_swappable<Pred>::valuenoexcept(swap(declval<Pred&>(),declval<Pred&>())));
        […]
      };
    }
    
  15. Change 23.5.6.1 [unord.set.overview] p3, class template unordered_set overview, as indicated:

    namespace std {
      template <class Key, 
                class Hash = hash<Key>,
                class Pred = std::equal_to<Key>,
                class Allocator = std::allocator<Key> >
      class unordered_set 
      {
        […]
        void swap(unordered_set&)
          noexcept(allocator_traits<Allocator>::is_always_equal::value &&
                   is_nothrow_swappable<Hash>::valuenoexcept(swap(declval<Hash&>(),declval<Hash&>())) &&
                   is_nothrow_swappable<Pred>::valuenoexcept(swap(declval<Pred&>(),declval<Pred&>())));
        […]
      };
    }
    
  16. Change 23.5.7.1 [unord.multiset.overview] p3, class template unordered_multiset overview, as indicated:

    namespace std {
      template <class Key, 
                class Hash = hash<Key>,
                class Pred = std::equal_to<Key>,
                class Allocator = std::allocator<Key> >
      class unordered_multiset 
      {
        […]
        void swap(unordered_multiset&)
          noexcept(allocator_traits<Allocator>::is_always_equal::value &&
                   is_nothrow_swappable<Hash>::valuenoexcept(swap(declval<Hash&>(),declval<Hash&>())) &&
                   is_nothrow_swappable<Pred>::valuenoexcept(swap(declval<Pred&>(),declval<Pred&>())));
        […]
      };
    }
    
  17. Change 23.6.3.1 [queue.defn] p1, class template queue definition, as indicated:

    namespace std {
      template <class T, class Container = deque<T> >
      class queue 
      {
        […]
      protected:
        Container c;
        
      public:
        […]
        void swap(queue& q) noexcept(is_nothrow_swappable<Container>::valuenoexcept(swap(c, q.c)))
        { using std::swap; swap(c, q.c); }
      };
    }
    
  18. Change 23.6.4 [priority.queue] p1, class template priority_queue definition, as indicated:

    namespace std {
      template <class T, class Container = vector<T>,
        class Compare = less<typename Container::value_type> >
      class priority_queue 
      {
        […]
      protected:
        Container c;
        Compare comp;
        
      public:
        […]
        void swap(priority_queue& q) noexcept(
            is_nothrow_swappable<Container>::value && is_nothrow_swappable<Compare>::value
            noexcept(swap(c, q.c)) && noexcept(swap(comp, q.comp)))
          { using std::swap; swap(c, q.c); swap(comp, q.comp); }
      };
    }
    
  19. Change 23.6.5.2 [stack.defn], class template stack definition, as indicated:

    namespace std {
      template <class T, class Container = deque<T> >
      class stack 
      {
        […]
      protected:
        Container c;
        
      public:
        […]
        void swap(stack& s) noexcept(is_nothrow_swappable<Container>::valuenoexcept(swap(c, s.c)))
          { using std::swap; swap(c, s.c); }
      };
    }
    

Feature-testing Macro

For the purposes of SG10, this paper recommends the macro name __cpp_lib_is_swappable.