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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
Niall Douglas
  • 9,212
  • 2
  • 44
  • 54
  • 1
    What result do you expect for overloaded callables which provide both ? – Quentin Jun 12 '15 at 12:06
  • @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. – Niall Douglas Jun 12 '15 at 12:50
  • Does the callable only take one argument? – T.C. Jun 12 '15 at 13:21
  • @T.C. In my particular use case here it's always a single argument. – Niall Douglas Jun 12 '15 at 13:41
  • @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. – Niall Douglas Jun 12 '15 at 13:44
  • 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). – bogdan Jun 12 '15 at 13:49
  • @bogdan You have exactly the problem I am stuck with. Let's keep it to distinguishing between the lambdas [](auto) and [](auto &&). – Niall Douglas Jun 12 '15 at 13:50
  • @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. – bogdan Jun 12 '15 at 14:01
  • @bogdan 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. – Niall Douglas Jun 12 '15 at 14:10
  • http://coliru.stacked-crooked.com/a/6ba8590ddbfb99d9 – T.C. Jun 12 '15 at 14:22
  • 5
    `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? – Yakk - Adam Nevraumont Jun 12 '15 at 14:38
  • @T.C. The link you posted shows lots of compile errors, is that expected? – Niall Douglas Jun 12 '15 at 15:03
  • @Yakk See Edit 2 to question above. If you have some method of overloading bind() to differentiate between call specifications, I am all ears. – Niall Douglas Jun 12 '15 at 15:04
  • @NiallDouglas Yes. The point is to use those error messages to show which type `first_arg* stuff */>::result` is. – T.C. Jun 12 '15 at 15:11
  • @T.C. 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? – Niall Douglas Jun 12 '15 at 15:25
  • So, that still isn't sufficiently concrete. Write out the *entire* use case. `auto m = monad.bind([](auto&&x){std::cout< – Yakk - Adam Nevraumont Jun 12 '15 at 15:40
  • @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. – Niall Douglas Jun 12 '15 at 16:01
  • 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. – T.C. Jun 12 '15 at 16:18
  • @T.C. You're right, I just tried your code using Microsoft's internal VS2015 at http://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. – Niall Douglas Jun 12 '15 at 16:28
  • @T.C. Really excellent, this modified solution http://melpon.org/wandbox/permlink/Yn7w4Vl1WLb1XKia works on GCC, clang and VS2015. Thanks T.C. – Niall Douglas Jun 12 '15 at 16:32

1 Answers1

7

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

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • 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. – Niall Douglas Jun 12 '15 at 23:55
  • @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`. – T.C. Jun 15 '15 at 03:21
  • Thanks for the improvement. You can see your answer, albeit much reworked into something less brittle, in the hopefully forthcoming Boost monad at https://github.com/ned14/boost.spinlock/blob/master/include/boost/spinlock/monad.hpp#L88 for those interested. And you're in the acknowledgements in the class docs. – Niall Douglas Jun 15 '15 at 10:30