This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of WP status.

3175. The CommonReference requirement of concept SwappableWith is not satisfied in the example

Section: 18.4.9 [concept.swappable] Status: WP Submitter: Kostas Kyrimis Opened: 2018-12-14 Last modified: 2020-09-06

Priority: 1

View all other issues in [concept.swappable].

View all issues with WP status.

Discussion:

The defect stems from the example found in sub-clause 18.4.9 [concept.swappable] p5:

[…]

template<class T, std::SwappableWith<T> U>
void value_swap(T&& t, U&& u) {
  ranges::swap(std::forward<T>(t), std::forward<U>(u));
}

[…]
namespace N {
  struct A { int m; };
  struct Proxy { A* a; };
  Proxy proxy(A& a) { return Proxy{ &a }; }
  
  void swap(A& x, Proxy p) {
    ranges::swap(x.m, p.a->m);
  }
  void swap(Proxy p, A& x) { swap(x, p); } // satisfy symmetry requirement
}

int main() {
  […]
  N::A a1 = { 5 }, a2 = { -5 };
  value_swap(a1, proxy(a2)); // diagnostic manifests here(#1)
  assert(a1.m == -5 && a2.m == 5);
}

The call to value_swap(a1, proxy(a2)) resolves to [T = N::A&, U = N::Proxy] The compiler will issue a diagnostic for #1 because:

  1. rvalue proxy(a2) is not swappable

  2. concept SwappableWith<T, U> requires N::A and Proxy to model CommonReference<const remove_reference_t<T>&, const remove_reference_t<U>&> It follows from the example that there is no common reference for [T = N::A&, U = N::Proxy]

[2019-06-20; Casey Carter comments and provides improved wording]

The purpose of the CommonReference requirements in the cross-type concepts is to provide a sanity check. The fact that two types satisfy a single-type concept, have a common reference type that satisfies that concept, and implement cross-type operations required by the cross-type flavor of that concept very strongly suggests the programmer intends them to model the cross-type concept. It's an opt-in that requires some actual work, so it's unlikely to be inadvertent.

The CommonReference<const T&, const U&> pattern makes sense for the comparison concepts which require that all variations of const and value category be comparable: we use const lvalues to trigger the "implicit expression variation" wording in 18.2 [concepts.equality]. SwappableWith, however, doesn't care about implicit expression variations: it only needs to witness that we can exchange the values denoted by two reference-y expressions E1 and E2. This suggests that CommonReference<decltype((E1)), decltype((E2))> is a more appropriate requirement than the current CommonReference<const remove_reference_t<…> mess that was blindly copied from the comparison concepts.

We must change the definition of "exchange the values" in 18.4.9 [concept.swappable] — which refers to the common reference type — consistently.

Previous resolution [SUPERSEDED]:

This wording is relative to N4791.

  1. Change 18.4.9 [concept.swappable] as indicated:

    -3- […]

    template<class T>
      concept Swappable = requires(T& a, T& b) { ranges::swap(a, b); };
      
    template<class T, class U>
      concept SwappableWith =
      CommonReference<T, Uconst remove_reference_t<T>&, const remove_reference_t<U>&> &&
      requires(T&& t, U&& u) {
        ranges::swap(std::forward<T>(t), std::forward<T>(t));
        ranges::swap(std::forward<U>(u), std::forward<U>(u));
        ranges::swap(std::forward<T>(t), std::forward<U>(u));
        ranges::swap(std::forward<U>(u), std::forward<T>(t));
      };
    

    -4- […]

    -5- [Example: User code can ensure that the evaluation of swap calls is performed in an appropriate context under the various conditions as follows:

    #include <cassert>
    #include <concepts>
    #include <utility>
    
    namespace ranges = std::ranges;
    
    template<class T, std::SwappableWith<T> U>
    void value_swap(T&& t, U&& u) {
      ranges::swap(std::forward<T>(t), std::forward<U>(u));
    }
    
    template<std::Swappable T>
    void lv_swap(T& t1, T& t2) {
      ranges::swap(t1, t2);
    }
    
    namespace N {
      struct A { int m; };
      struct Proxy { 
        A* a;
        Proxy(A& a) : a{&a} {}
        friend void swap(Proxy&& x, Proxy&& y) {
          ranges::swap(x.a, y.a);
        }
      };
      Proxy proxy(A& a) { return Proxy{ &a }; }
      void swap(A& x, Proxy p) {
        ranges::swap(x.m, p.a->m);
      }
      void swap(Proxy p, A& x) { swap(x, p); } // satisfy symmetry requirement
    }
    
    int main() {
      int i = 1, j = 2;
      lv_swap(i, j);
      assert(i == 2 && j == 1);
      N::A a1 = { 5 }, a2 = { -5 };
      value_swap(a1, proxy(a2));
      assert(a1.m == -5 && a2.m == 5);
    }
    

[2020-01-16 Priority set to 1 after discussion on the reflector.]

[2020-02-10 Move to Immediate Monday afternoon in Prague]

Proposed resolution:

This wording is relative to N4820.

  1. Change 18.4.9 [concept.swappable] as indicated:

    -1- Let t1 and t2 be equality-preserving expressions that denote distinct equal objects of type T, and let u1 and u2 similarly denote distinct equal objects of type U. [Note: t1 and u1 can denote distinct objects, or the same object. — end note] An operation exchanges the values denoted by t1 and u1 if and only if the operation modifies neither t2 nor u2 and:

    1. (1.1) — If T and U are the same type, the result of the operation is that t1 equals u2 and u1 equals t2.

    2. (1.2) — If T and U are different types that model CommonReference<const T&, const U&>and CommonReference<decltype((t1)), decltype((u1))> is modeled, the result of the operation is that C(t1) equals C(u2) and C(u1) equals C(t2) where C is common_reference_t<const T&, const U&decltype((t1)), decltype((u1))>.

    -2- […]

    -3- […]

    template<class T>
      concept Swappable = requires(T& a, T& b) { ranges::swap(a, b); };
      
    template<class T, class U>
      concept SwappableWith =
      CommonReference<T, Uconst remove_reference_t<T>&, const remove_reference_t<U>&> &&
      requires(T&& t, U&& u) {
        ranges::swap(std::forward<T>(t), std::forward<T>(t));
        ranges::swap(std::forward<U>(u), std::forward<U>(u));
        ranges::swap(std::forward<T>(t), std::forward<U>(u));
        ranges::swap(std::forward<U>(u), std::forward<T>(t));
      };
    

    -4- […]

    -5- [Example: User code can ensure that the evaluation of swap calls is performed in an appropriate context under the various conditions as follows:

    #include <cassert>
    #include <concepts>
    #include <utility>
    
    namespace ranges = std::ranges;
    
    template<class T, std::SwappableWith<T> U>
    void value_swap(T&& t, U&& u) {
      ranges::swap(std::forward<T>(t), std::forward<U>(u));
    }
    
    template<std::Swappable T>
    void lv_swap(T& t1, T& t2) {
      ranges::swap(t1, t2);
    }
    
    namespace N {
      struct A { int m; };
      struct Proxy { 
        A* a;
        Proxy(A& a) : a{&a} {}
        friend void swap(Proxy x, Proxy y) {
          ranges::swap(*x.a, *y.a);
        }
      };
      Proxy proxy(A& a) { return Proxy{ &a }; }
      void swap(A& x, Proxy p) {
        ranges::swap(x.m, p.a->m);
      }
      void swap(Proxy p, A& x) { swap(x, p); } // satisfy symmetry requirement
    }
    
    int main() {
      int i = 1, j = 2;
      lv_swap(i, j);
      assert(i == 2 && j == 1);
      N::A a1 = { 5 }, a2 = { -5 };
      value_swap(a1, proxy(a2));
      assert(a1.m == -5 && a2.m == 5);
    }