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
Ecan be implicitly converted to a typeTif 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_vwrong?
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.
