How to determine if any call being called accepts an rvalue reference?

I am trying to write a trait that determines if some rvalue being called refers to as its first parameter. This allows some metaprogramming to configure whether to use the semantics of moving or copying when calling the called, where the called is supplied with external code (in fact, one of them overloads the called type provided by the 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 hopefully explains what I'm looking for: I want to detect calls that limit their parameter to only the rvalue reference.

Please note that other stack overflow answers like Get parameter type lambda are not useful here, because I need to support common lambdas characters in C ++ 14 (i.e. those that take automatic parameters), and so the tricks based on accepting the call operator address inside the lambda type will fail with the inability to allow overloading.

You will notice that is_callable_working accepts an Arg type, and the correct overload of the called F will be found through F(Arg) . What I would like to discover is the available overload for F(Arg) - this is F::operator()(Arg &&) or F::operator()( and any other reference type for Arg> ) . I would suggest that if ambiguous overloads are available for F() , for example. like F(Arg) and F(Arg &&) , then the compiler will fail, however [](auto) should not be ambiguous from [](auto &&) .

Edit: My question is streamlined. I really ask if C ++ metaprogramming can detect constraints on arguments.

Edit 2: Here are some more clarifications. My specific use case:

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

where monad<T>.bind([](T}{}) accepts T by copying, and I would like monad<T>.bind([](T &&){}) accept T by rvalue (t .e. the caller could move from him).

As was done above, I would also like monad<T>.bind([](auto){}) take T copies and monad<T>.bind([](auto &&){}) so that take T from the rvalue link.

As I mentioned, this is a kind of overload of monad<T>.bind() , resulting in various effects depending on how the called is specified. If it were possible to overload bind() based on the call signature, as we could before lambdas, all of this would be easy. This is about the unrecognizability of capturing lambda types, which is the problem here.

+5
source share
1 answer

This should work for most sane lambdas (and, in addition, things that are quite similar to 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" = there are no crazy things like const auto&& or volatile .

rank used to control congestion resolution - a viable congestion with the highest rank is selected.

First, consider the high-level test overloads, which are functional patterns. If F::operator() is a template, then the first argument is an irreducible context (by [temp.deduct.call] /p6.1), and therefore T cannot be inferred, and they are removed from the overload resolution.

If F::operator() not a template, deduction is performed, the corresponding overload is selected, and the type of the first parameter is encoded in the return type of the function. Ranks effectively establish an if-else-if relationship:

  • If the first argument is a rvalue reference, the output will succeed for one of the two overloads from rank 12 to 12, so it will be selected;
  • Otherwise, the deduction will fail for overloads of rank 12. If the first argument is an lvalue reference, the output will succeed for one of the overloads of rank 11, and this choice will be selected;
  • Otherwise, the first argument will be by value, and the output will succeed to overload rank 10.

Note that we leave ranking 10 last, because the output will always be successful for this, regardless of the nature of the first argument - it can infer T as a reference type. (In fact, we would get the correct result if we did six template overloads, all have the same rank, due to partial ordering rules, but IMO is easier to understand this way.)

Now to the low-level test overloads, which have hardcoded types of member pointers as their first parameter. They really are played only if F::operator() is a template (if this is not the case, then overloaded with a higher rating prevail). Passing the address of the function template to these functions leads to the conclusion of the template argument for this function template to obtain the type of function that corresponds to the type of parameter (see [Over.over]).

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

  • If a function template can be created to accept a non-primary arg_type reference, then it must be (auto) (rank 6);
  • Otherwise, if the function template can be created for something using the reference type rvalue arg_type&& , then it should be (auto&&) (rank 5);
  • Otherwise, if the function template can be created for something using the non-constant-qualified arg_type& , then it should be (auto&) (rank 4);
  • Otherwise, if the function template can be created using const arg_type& , then it must be (const auto&) (rank 3).

Here we again handle the case (auto) , because otherwise it can be created to form the other three signatures. Moreover, we handle the case (auto&&) before the case (auto&) , because the forwarding rules apply for this output, and auto&& can be inferred from arg_type& .

+7
source

All Articles