Why isn't std::swap marked constexpr before C++20?

The strange language issue is CWG 1581:

Clause 15 [special] is perfectly clear that special member functions are only implicitly defined when they are odr-used. This creates a problem for constant expressions in unevaluated contexts:

struct duration {
  constexpr duration() {}
  constexpr operator int() const { return 0; }
};

// duration d = duration(); // #1
int n = sizeof(short{duration(duration())});

The issue here is that we are not permitted to implicitly define constexpr duration::duration(duration&&) in this program, so the expression in the initializer list is not a constant expression (because it invokes a constexpr function which has not been defined), so the braced initializer contains a narrowing conversion, so the program is ill-formed.

If we uncomment line #1, the move constructor is implicitly defined and the program is valid. This spooky action at a distance is extremely unfortunate. Implementations diverge on this point.

You can read the rest of the issue description.

A resolution for this issue was adopted in P0859 in Albuquerque in 2017 (after C++17 shipped). That issue was a blocker for both being able to have a constexpr std::swap (resolved in P0879) and a constexpr std::invoke (resolved in P1065, which also has CWG1581 examples), both for C++20.


The simplest to understand example here, in my opinion, is the code from the LLVM bug report pointed out in P1065:

template<typename T>
int f(T x)
{
    return x.get();
}

template<typename T>
constexpr int g(T x)
{
    return x.get();
}

int main() {

  // O.K. The body of `f' is not required.
  decltype(f(0)) a;

  // Seems to instantiate the body of `g'
  // and results in an error.
  decltype(g(0)) b;

  return 0;
}

CWG1581 is all about when constexpr member functions are defined, and the resolution ensures that they're only defined when used. After P0859, the above is well-formed (the type of b is int).

Since std::swap and std::invoke both have to rely upon checking for member functions (move construction/assignment in the former and the call operator/surrogate calls in the latter), they both depended on the resolution of this issue.


The reason

(due to @NathanOliver)

To allow a constexpr swap function, you have to check - before instantiating the template for this function - that the swapped type is move-constructible and move-assignable. Unfortunately, due to a language defect only resolved in C++20, you can't check for that, since the relevant member functions may not have been defined yet, as far as the compiler is concerned.

The chronology

  • 2016: Antony Polukhin submitts proposal P0202, to mark all of the <algorithm> functions as constexpr.
  • The core working group of the standard committee discusses defect CWG-1581. This issue made it problematic to have constexpr std::swap() and also constexpr std::invoke() - see explanation above.
  • 2017: Antony revises his proposal a few times to exclude std::swap and some other constructs, and this is accepted into C++17.
  • 2017: A resolution for CWG-1581 issue is submitted as P0859 and accepted by the standard committee in 2017 (but after C++17 shipped).
  • End of 2017: Antony submits a complementary proposal, P0879, to make std::swap() constexpr after the resolution of CWG-1581.
  • 2018: The complementary proposal is accepted (?) into C++20. As Barry points out, so is the constexpr std::invoke() fix.

Your specific case

You can use constexpr swapping if you don't check for move-constructibility and move-assignability, but rather directly check for some other feature of types which ensures that in particular. e.g. only primitive types and no classes or structs. Or, theoretically, you could forego the checks and just deal with any compilation errors you might encounter, and with flaky behavior switching between compilers. In any case, don't replace std::swap() with that kind of a thing.