How to detect whether some callable takes a rvalue reference?
Asked Answered
G

1

5

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.

Garratt answered 12/6, 2015 at 11:51 Comment(20)
What result do you expect for overloaded callables which provide both ?Kreis
@Quentin: You might note the second parameter in is_callable_moving is the arg with which to call the callable in order to have the correct overload selected and/or the auto lambda instantiated correctly. The hope would be that callable(auto) could be distinguished from callable(auto &&) somehow.Garratt
Does the callable only take one argument?Kreisler
@Kreisler In my particular use case here it's always a single argument.Garratt
@Barry I agree it's tricky. One would have thought there some way in C++ 14 to get the compiler to instantiate a template function and then extract its call signature, but maybe we're waiting on Reflection for that.Garratt
Do you want to detect whether the callable takes a plain rvalue reference or a forwarding reference? 'Cause auto&& is the latter (it binds to both rvalues and lvalues).Dennadennard
@Dennadennard You have exactly the problem I am stuck with. Let's keep it to distinguishing between the lambdas [](auto) and [](auto &&).Garratt
@NiallDouglas Well, strictly that last part should work - feed it an argument of a non-copyable non-movable type, which should fail for the first one and work for the second.Dennadennard
@Dennadennard What a clever idea! Yes, that could work for the auto lambda case. However my use case allows any callable of the form F(Arg), F(Arg &&), F(auto), F(auto &&) where Arg is the type of *this for the member function. I'll edit the question about to clarify some more.Garratt
coliru.stacked-crooked.com/a/6ba8590ddbfb99d9Kreisler
auto moving = [](auto&&) {}; // Takes a constrained to rvalue ref input. -- this is not constrained to rvalue ref input. What the callable consumes shouldn't matter to the calling code. If you (and others) aren't using the data again, you should always move. If you (and others) are using the data again, you should not move. This is information about the data you are calling with, not about the thing you are calling. And forward and reference_wrapper exist to deal with the corner cases. Can you be less abstract in what you want? What concrete problem are you solving?Subshrub
@Kreisler The link you posted shows lots of compile errors, is that expected?Garratt
@Yakk See Edit 2 to question above. If you have some method of overloading bind() to differentiate between call specifications, I am all ears.Garratt
@NiallDouglas Yes. The point is to use those error messages to show which type first_arg</* stuff */>::result is.Kreisler
@Kreisler Oh I see now. It's printing exactly what the first arg's reference type is. This looks to me like you solved the problem, but I am surprised. In my own efforts any attempt to do decltype(&F::operator()) failed with an ambiguous overload error, but you seem to have done some magic with an intermediate test() shim which seems to choose a test() overload for you, thus avoiding the ambiguous overload problem. Can you explain how your solution works?Garratt
So, that still isn't sufficiently concrete. Write out the entire use case. auto m = monad<int>.bind([](auto&&x){std::cout<<decltype(x)(x)<<'\n';}); int x = 3; m(x); -- from what I can tell, you are asking for the code I just wrote should fail to compile. This seems like a bad idea. But maybe I misunderstood you, because you are not being very concrete. You are focusing on narrow technical problems, which is great, but your problem also needs to be practical.Subshrub
@Yakk: T.C. has already answered the question with a working solution judging from his link. I just need him to explain how his solution works, because I don't get why it works. I think he's using Expression SFINAE, something I've never used personally due to me needing MSVC compatibility.Garratt
Actually it is not expression SFINAE; briefly, the idea is to put &F::operator() into a context that causes overload resolution and template argument deduction to happen. I'll write a full answer later when I get back to my computer.Kreisler
@Kreisler You're right, I just tried your code using Microsoft's internal VS2015 at webcompiler.cloudapp.net and it looks to me it compiles your code just fine, it just isn't printing what we need it to. I might fiddle with your source to get a portable solution.Garratt
@Kreisler Really excellent, this modified solution melpon.org/wandbox/permlink/Yn7w4Vl1WLb1XKia works on GCC, clang and VS2015. Thanks T.C.Garratt
K
8

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

Kreisler answered 12/6, 2015 at 18:51 Comment(3)
I think you need to also explain how and why the ranking works as it does, and then we'll have a very good answer indeed. And thank you very much T.C., I'll credit you in the docs for the help and you should see this monad announced on boost-dev as the first step in next gen lightweight future-promises early next week. We'll see how the community like it, or not.Garratt
@NiallDouglas I added some extra explanations and tweaked the ranking of the six templated overloads of test a bit (which also got rid of the enable_if.Kreisler
Thanks for the improvement. You can see your answer, albeit much reworked into something less brittle, in the hopefully forthcoming Boost monad<T> at github.com/ned14/boost.spinlock/blob/master/include/boost/… for those interested. And you're in the acknowledgements in the class docs.Garratt

© 2022 - 2024 — McMap. All rights reserved.