Search code examples
c++language-lawyerc++20sfinaetype-traits

Why does std::is_invocable_r reject functions returning non-moveable types?


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 to R.

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 type T if and only if the declaration T t=E; is well-formed, for some invented temporary variable t ([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?


Solution

  • 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.