Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

In C++20, std::swap becomes a constexpr function.

I know that the standard library really lagged behind the language in marking things constexpr, but by 2017, <algorithm> was pretty much constexpr as were a bunch of other things. Yet - std::swap wasn't. I vaguely remember there being some strange language defect which prevent that marking, but I forget the details.

Can someone explain this succinctly and clearly?

Motivation: Need to understand why it may be bad idea to mark an std::swap()-like function constexpr in C++11/C++14 code.

like image 343
einpoklum Avatar asked Mar 13 '20 17:03

einpoklum


People also ask

Can std :: function be Constexpr?

Constexpr constructors are permitted for classes that aren't literal types. For example, the default constructor of std::unique_ptr is constexpr, allowing constant initialization.

Why should I use constexpr?

A constexpr integral value can be used wherever a const integer is required, such as in template arguments and array declarations. And when a value is computed at compile time instead of run time, it helps your program run faster and use less memory.

What does std :: swap do?

The function std::swap() is a built-in function in the C++ Standard Template Library (STL) which swaps the value of two variables. Parameters: The function accepts two mandatory parameters a and b which are to be swapped. The parameters can be of any data type.


2 Answers

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.

like image 87
Barry Avatar answered Sep 23 '22 06:09

Barry


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.

like image 37
einpoklum Avatar answered Sep 22 '22 06:09

einpoklum