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.
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.
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.
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.
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.
(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.
<algorithm>
functions as constexpr
.constexpr std::swap()
and also constexpr std::invoke()
- see explanation above.std::swap
and some other constructs, and this is accepted into C++17.std::swap()
constexpr after the resolution of CWG-1581.std::invoke()
fix.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With