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.
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:
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:
arg_type
, then it must be (auto)
(rank 6);arg_type&&
, then it must be (auto&&)
(rank 5);arg_type&
, then it must be (auto&)
(rank 4);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&
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With