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