How to detect whether some callable takes a rvalue reference?

737 views Asked by At

I have been trying to write a trait which figures out whether some callable takes a rvalue reference as its first parameter. This lets some metaprogramming adjust whether move or copy semantics are used when calling the callable where the callable is supplied by external code (effectively one is overloading on the callable type supplied by a user).

#include <functional>
#include <iostream>
#include <type_traits>

// Does the callable when called with Arg move?
template<class F, class Arg> struct is_callable_moving
{
  typedef typename std::decay<Arg>::type arg_type;
  typedef typename std::function<F(arg_type)>::argument_type parameter_type;
  static constexpr bool value = std::is_rvalue_reference<parameter_type>::value;
};

int main(void)
{
  auto normal = [](auto) {};    // Takes an unconstrained input.
  auto moving = [](auto&&) {};  // Takes a constrained to rvalue ref input.
  std::cout << "normal=" << is_callable_moving<decltype(normal), int>::value << std::endl;
  std::cout << "moving=" << is_callable_moving<decltype(moving), int>::value << std::endl;  // should be 1, but isn't
  getchar();
  return 0;
}

The above obviously does not work, but it hopefully explains what I am looking for: I want to detect callables which constrain their parameter to only being a rvalue reference.

Note that other Stack Overflow answers such as Get lambda parameter type aren't useful here because I need to support C++ 14 generic lambdas (i.e. the ones taking auto parameters) and therefore tricks based on taking the address of the call operator inside the lambda type will fail with inability to resolve overload.

You will note that is_callable_working takes an Arg type, and the correct overload of the callable F would be found via F(Arg). The thing I'd like to detect is whether the available overload for F(Arg) is a F::operator()(Arg &&) or a F::operator()(<any other reference type for Arg>). I would imagine that if ambiguous overloads for F() are available e.g. both F(Arg) and F(Arg &&) then the compiler would error out, however a [](auto) should not be ambiguous from [](auto &&).

Edit: Clarified my question hopefully. I'm really asking if C++ metaprogramming can detect constraints on arguments.

Edit 2: Here is some more clarification. My exact use case is this:

template<class T> class monad
{
  ...
  template<class U> monad<...> bind(U &&v);
};

where monad<T>.bind([](T}{}) takes T by copy, and I'd like monad<T>.bind([](T &&){}) takes T by rvalue reference (i.e. the callable could move from it).

As inferred above, I'd also like monad<T>.bind([](auto){}) to take T by copy, and monad<T>.bind([](auto &&){}) to take T by rvalue reference.

As I mentioned, this is a sort of overload of monad<T>.bind() whereby different effects occur depending on how the callable is specified. If one were able to overload bind() based on call signature as we could before lambdas, all this would be easy. It's dealing with the unknowability of capturing lambda types which is the problem here.

1

There are 1 answers

3
T.C. On BEST ANSWER

This should work for most sane lambdas (and by extension, things that are sufficiently like lambdas):

struct template_rref {};
struct template_lref {};
struct template_val {};

struct normal_rref{};
struct normal_lref{};
struct normal_val{};

template<int R> struct rank : rank<R-1> { static_assert(R > 0, ""); };
template<> struct rank<0> {};

template<class F, class A>
struct first_arg {

    using return_type = decltype(std::declval<F>()(std::declval<A>()));
    using arg_type = std::decay_t<A>;


    static template_rref test(return_type (F::*)(arg_type&&), rank<5>);
    static template_lref test(return_type (F::*)(arg_type&), rank<4>);
    static template_lref test(return_type (F::*)(const arg_type&), rank<3>);
    static template_val test(return_type (F::*)(arg_type), rank<6>);

    static template_rref test(return_type (F::*)(arg_type&&) const, rank<5>);
    static template_lref test(return_type (F::*)(arg_type&) const, rank<4>);
    static template_lref test(return_type (F::*)(const arg_type&) const, rank<3>);
    static template_val test(return_type (F::*)(arg_type) const, rank<6>);

    template<class T>
    static normal_rref test(return_type (F::*)(T&&), rank<12>);
    template<class T>
    static normal_lref test(return_type (F::*)(T&), rank<11>);
    template<class T>
    static normal_val test(return_type (F::*)(T), rank<10>);

    template<class T>
    static normal_rref test(return_type (F::*)(T&&) const, rank<12>);
    template<class T>
    static normal_lref test(return_type (F::*)(T&) const, rank<11>);
    template<class T>
    static normal_val test(return_type (F::*)(T) const, rank<10>);

    using result = decltype(test(&F::operator(), rank<20>()));
};

"sane" = no crazy stuff like const auto&& or volatile.

rank is used to help manage overload resolution - the viable overload with the highest rank is selected.

First consider the highly-ranked test overloads that are function templates. If F::operator() is a template, then the first argument is a non-deduced context (by [temp.deduct.call]/p6.1), and so T cannot be deduced, and they are removed from overload resolution.

If F::operator() isn't a template, then deduction is performed, the appropriate overload is selected, and the type of the first parameter is encoded in the function's return type. The ranks effectively establish an if-else-if relationship:

  • If the first argument is an rvalue reference, deduction will succeed for one of the two rank 12 overloads, so it's chosen;
  • Otherwise, deduction will fail for the rank 12 overloads. If the first argument is an lvalue reference, deduction will succeed for one of the rank 11 overloads, and that one is chosen;
  • Otherwise, the first argument is by value, and deduction will succeed for the rank 10 overload.

Note that we leave rank 10 last because deduction will always succeed for that one regardless of the nature of the first argument - it can deduce T as a reference type. (Actually, we'd get the right result if we made the six template overloads all have the same rank, due to partial ordering rules, but IMO it's easier to understand this way.)

Now to the lowly-ranked test overloads, which have hard-coded pointer-to-member-function types as their first parameter. These are only really in play if F::operator() is a template (if it isn't then the higher-ranked overloads will prevail). Passing the address of a function template to these functions causes template argument deduction to be performed for that function template to obtain a function type that matches the parameter type (see [over.over]).

We consider the [](auto){}, [](auto&){}, [](const auto&){} and [](auto&&){} cases. The logic encoded in the ranks is as follows:

  • If the function template can be instantiated to take a non-reference arg_type, then it must be (auto) (rank 6);
  • Else, if the function template can be instantiated to something taking an rvalue reference type arg_type&&, then it must be (auto&&) (rank 5);
  • Else, if the function template can be instantiated to something taking a non-const-qualified arg_type&, then it must be (auto&) (rank 4);
  • Else, if the function template can be instantiated to something taking a const arg_type&, then it must be (const auto&) (rank 3).

Here, again, we handle the (auto) case first because otherwise it can be instantiated to form the three other signatures as well. Moreover, we handle the (auto&&) case before the (auto&) case because for this deduction the forwarding reference rules apply, and auto&& can be deduced from arg_type&.