Does throw x
implicit-move? Let’s ask SFINAE
throw x
implicit-move? Let’s ask SFINAELanguage lawyers may want to read the UPDATE at the bottom of this post before the rest. I think everything discussed here is, technically speaking, ill-formed.
As of C++20, thanks to P1155 “More implicit move,”
throw x
gets the same implicit-move semantics as return x
. So you can do, for example,
std::unique_ptr<int> a(std::unique_ptr<int> p)
{
auto v = std::make_unique<int>(1);
return v; // OK: implicit move (since C++11ish)
return p; // OK: implicit move (since C++11ish)
}
void b(std::unique_ptr<int> p)
{
auto v = std::make_unique<int>(1);
throw v; // OK: implicit move (since C++14ish)
throw p; // OK: implicit move (since C++20)
}
Here’s a handy table of compiler support for implicit move in each of these situations;
i.e., does the compiler accept the given expression for a move-only type like unique_ptr
?
Answer: Yes, in all modes — but GCC’s dev team likes to complicate things. GCC alone sticks
to the formal C++17 standard and rejects throw p
pre-C++20.
Expression | GCC | Clang | MSVC | ICC |
---|---|---|---|---|
return v | ∞ | ∞ | ∞ | ∞ |
return p | ∞ | ∞ | ∞ | ∞ |
throw v | ∞ | ∞ | ∞ | ∞ |
throw p | 20+ | ∞ | ∞ | ∞ |
This week Erich Keane pushed me to think about how the (past and future) changes to “implicit move” may affect SFINAE. Consider the following test program (Godbolt):
template<class T>
auto f(T p, int) -> decltype(throw p)
{
puts("one"); // #1
throw p;
}
template<class T>
auto f(T p, long) -> void
{
puts("two"); // #2
throw p;
}
int main() {
f(std::make_unique<int>(42), 42);
}
The best-matching overload is #1, but it participates only when throw p
is well-formed. (Btw: when
throw p
is well-formed, its decltype is invariably void
.) When throw p
is ill-formed, #1 drops out
of the overload set; #2 is the best remaining match.
-
Clang (in all modes) accepts the program and calls #1.
-
MSVC (in all modes) accepts the program and calls #1.
-
Intel ICC 2021.1.2 (in all modes) accepts the program and calls #2.
-
GCC (in all modes, including C++20) gives a hard error trying to check the well-formedness of
throw p
. It reports that the expression attempted to callunique_ptr
’s deleted copy constructor — which is correct — but I’m surprised that GCC doesn’t consider this to be an “immediate context” inside which “substitution failure is not an error.”
See, the well-formedness of throw p
depends on whether you think it’s allowed to implicit-move from p
.
If implicit move is allowed on the operand of throw
, inside an unevaluated expression, then it’s well-formed
(this is what Clang and MSVC think); otherwise it’s ill-formed (this is what ICC thinks); and I have no idea
what GCC is thinking.
ICC 19.0.1’s behavior actually matched GCC’s: hard error in all modes!
There’s a disturbing parallel here to decltype(co_await x)
. The Committee decided that co_await
was
context-sensitive enough that C++20 forbids all use of the co_await
operator inside unevaluated expressions.
throw
will never reach co_await
’s extreme level of context-dependence; but it is a little
context-dependent, and that is bad because it produces implementation divergence.
Looking ahead to P2266 “Simpler implicit move”
Fortunately, the divergence has already happened. (The program above is only C++14, but gives three different answers on Clang/MSVC, ICC, and GCC.) My hope is that by simplifying the rules around “implicit move” (particularly, junking the “two overload resolutions” dance), P2266 “Simpler Implicit Move” will lead to implementation convergence.
Here is a program that uses SFINAE to detect whether the compiler is fully in P2266-world. (Note that in real life, you wouldn’t use metaprogramming for this; you’d just check the feature-test macro.) Godbolt:
struct AutoPtr {
AutoPtr() = default;
AutoPtr(AutoPtr&) {}
};
template<class T>
auto f(T p, int) -> decltype(throw p, 1) { return 1; }
template<class T>
int f(T p, long) { return 2; }
int main() {
return f(AutoPtr(), 42);
}
In C++20, all vendors agree that main
returns 1. In P2266-world, the intent is that throw p
be
ill-formed (because p
is a move-eligible id-expression, therefore an xvalue; but AutoPtr
is
constructible only from lvalues), and so main
should return 2.
At least, that’s how I think decltype(throw p)
should work. What do you think?
UPDATE, 2021-03-19: Reddit commenter “scatters” points out that C++20 requires
-expressions
permit us to express the trouble even more cleanly:
template<class T> requires requires (T p) { throw p; }
void f(T p) { return 1; }
template<class T>
void f(T p) { return 2; }
int main() { return f(AutoPtr()); }
Clang and MSVC call #1; GCC hard-errors; ICC doesn’t support requires
yet.
Interestingly, MSVC believes even that requires (T& p) { throw p; }
is true,
although it clearly is not. This discovery reminded me that actually everything discussed
in this entire post is ill-formed, diagnostic required. See
“MSVC can’t handle move-only exception types”
(2019-05-11) and [except.throw/5].