Search code examples
c++stllanguage-lawyerc++20customization-point

Why is deleting a function necessary when you're defining customization point object?


From libstdc++ <concepts> header:

  namespace ranges
  {
    namespace __cust_swap
    {
      template<typename _Tp> void swap(_Tp&, _Tp&) = delete;

From MS-STL <concepts> header:

namespace ranges {
    namespace _Swap {
        template <class _Ty>
        void swap(_Ty&, _Ty&) = delete;

I've never encountered = delete; outside the context where you want to prohibit the call to copy/move assignment/ctor.

I was curious if this was necessary, so I've commented out the = delete; part from the library like this:

// template<typename _Tp> void swap(_Tp&, _Tp&) = delete;

to see if the following test case compiles.

#include <concepts>
#include <iostream>

struct dummy {
    friend void swap(dummy& a, dummy& b) {
        std::cout << "ADL" << std::endl;
    }
};

int main()
{
    int a{};
    int b{};
    dummy c{};
    dummy d{};
    std::ranges::swap(a, b);
    std::ranges::swap(c, d); // Ok. Prints "ADL" on console.
}

Not only it compiles, it seems to behave well by calling user defined swap for struct dummy. So I'm wondering,

  1. What does template<typename _Tp> void swap(_Tp&, _Tp&) = delete; exactly do in this context?
  2. On what occasion does this break without template<typename _Tp> void swap(_Tp&, _Tp&) = delete;?

Solution

  • TL;DR: It's there to keep from calling std::swap.

    This is actually an explicit requirement of the ranges::swap customization point:

    S is (void)swap(E1, E2) if E1 or E2 has class or enumeration type ([basic.compound]) and that expression is valid, with overload resolution performed in a context that includes this definition:

     template<class T>
      void swap(T&, T&) = delete;
    

    So what does this do? To understand the point of this, we have to remember that the ranges namespace is actually the std::ranges namespace. That's important because a lot of stuff lives in the std namespace. Including this, declared in <utility>:

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

    There's probably a constexpr and noexcept on there somewhere, but that's not relevant for our needs.

    std::ranges::swap, as a customization point, has a specific way it wants you to customize it. It wants you to provide a swap function that can be found via argument-dependent lookup. Which means that ranges::swap is going to find your swap function by doing this: swap(E1, E2).

    That's fine, except for one problem: std::swap exists. In the pre-C++20 days, one valid way of making a type swappable was to provide a specialization for the std::swap template. So if you called std::swap directly to swap something, your specializations would be picked up and used.

    ranges::swap does not want to use those. It has one customization mechanism, and it wants you to very definitely use that mechanism, not template specialization of std::swap.

    However, because std::ranges::swap lives in the std namespace, unqualified calls to swap(E1, E2) can find std::swap. To avoid finding and using this overload, it poisons the overload by making visible a version that is = deleted. So if you don't provide an ADL-visible swap for your type, you get a hard error. A proper customization is also required to be more specialized (or more constrained) than the std::swap version, so that it can be considered a better overload match.

    Note that ranges::begin/end and similar functions have similar wording to shut down similar problems with similarly named std:: functions.