I'm curious about the definition of std::is_invocable_r
and how it interacts with non-moveable types. Its libc implementation under clang in C 20 mode seems to be wrong based on my understanding of the language rules it's supposed to emulate, so I'm wondering what's incorrect about my understanding.
Say we have a type that can't be move- or copy-constructed, and a function that returns it:
struct CantMove {
CantMove() = default;
CantMove(CantMove&&) = delete;
};
static_assert(!std::is_move_constructible_v<CantMove>);
static_assert(!std::is_copy_constructible_v<CantMove>);
CantMove MakeCantMove() { return CantMove(); }
Then it's possible to call that function to initialize a CantMove
object (I believe due to the copy elision rules):
CantMove cant_move = MakeCantMove();
And the type traits agree that the function is invocable and returns CantMove
:
using F = decltype(MakeCantMove);
static_assert(std::is_invocable_v<F>);
static_assert(std::is_same_v<CantMove, std::invoke_result_t<F>>);
But std::is_invocable_r
says it's not possible to invoke it to yield something convertible to CantMove
, at least in C 20 under clang:
static_assert(!std::is_invocable_r_v<CantMove, F>);
The definition of std::is_invocable_r
is
The expression
INVOKE<R>(declval<Fn>(), declval<ArgTypes>()...)
is well-formed when treated as an unevaluated operand
with INVOKE<R>
being defined as
Define
INVOKE<R>(f, t1, t2, …, tN)
as [...]INVOKE(f, t1, t2, …, tN)
implicitly converted toR
.
and INVOKE
defined (in this case) as simply MakeCantMove()
. But the definition of whether an implicit conversion is possible says:
An expression
E
can be implicitly converted to a typeT
if and only if the declarationT t=E;
is well-formed, for some invented temporary variablet
([dcl.init]).
But we saw above that CantMove cant_move = MakeCantMove();
is accepted by the compiler. So is clang wrong about accepting this initialization, or is the implementation of std::is_invocable_r_v
wrong? Or is my reading of the standard wrong?
For the record, the reason I care about this question is that types like std::move_only_function
(I'm using an advanced port to C 20 of this) have their members' overload sets restricted by std::is_invocable_r_v
, and I'm finding that it's not possible to usefully work with functions that return a no-move type like this. Is that by design, and if so why?
CodePudding user response:
So is clang wrong about accepting this initialization, or is the implementation of
std::is_invocable_r_v
wrong?
This is a bug of libc . In the implementation of is_invocable_r
, it uses is_convertible
to determine whether the result can be implicitly converted to T
, which is incorrect since is_convertible_v<T, T>
is false
for non-movable types, in which case std::declval
adds an rvalue reference to T
.
It is worth noting that both libstdc and MSVC-STL have bug reports about this issue, which have been fixed.