lightxbulb
lightxbulb

Reputation: 1321

is_invocable with arbitrary function argument types

Is there a way to use std::is_invocable with arbitrary function arguments types, something like: std::is_invocable<Function, auto>. The idea is to check whether Function can accept 1 argument, regardless of the type of the argument. For a use case consider two lambdas: auto lambda1 = [](auto x) {...}, auto lambda2 = [](auto x, auto y) {...}, and a higher order templated function:

// specialize for 1 argument
template<typename Function, std::enable_if_t<(std::is_invocable<Function, auto>::value && !std::is_invocable<Function, auto, auto>::value)>, bool> = true>
void higherOrderFunc(Function&& func);

// specialize for 2 arguments
template<typename Function, std::enable_if_t<std::is_invocable<Function, auto, auto>::value, bool> = true>
void higherOrderFunc(Function&& func);

The !std::is_invocable<Function, auto, auto>::value in the first case is to prevent ambiguity for overloaded functions (that is, the preferred specialization in this case would be the 2 argument one in case of ambiguity).

Note that I am aware that auto cannot be used like this in this case. I am asking whether there's a way to implement this behaviour (at least partially).

Upvotes: 2

Views: 2340

Answers (1)

max66
max66

Reputation: 66230

Maybe not a perfect solution... but you can try with a passepartout

struct passepartout
 {
   template <typename T>
   operator T & ();

   template <typename T>
   operator T && ();
 };

Observe that conversion operators are only declared, not defined; so this structure can be used in decltype() and with std::declval() (and std::is_invocable) but can't be instantiated.

Now you can write your higherOrderFunc passing reference to passepartout to std::is_invocable.

template <typename F,
   std::enable_if_t<
           std::is_invocable_v<F, passepartout &>
      && ! std::is_invocable_v<F, passepartout &, passepartout &>, bool>
         = true>
void higherOrderFunc (F)
 { std::cout << "-- one parameter callable" << std::endl; }

template <typename F,
   std::enable_if_t<
      std::is_invocable_v<F, passepartout &, passepartout &>, bool> = true>
void higherOrderFunc (F)
 { std::cout << "-- two parameter callable" << std::endl; }

The trick is that if a callable waits for auto (or auto &, or auto &&), the type is deduced as passepartout itself; when the callable wait a specific type (int, with or without references, in the following examples), the template operator T & () (or operator T && (), according the cases) is compatible (in a sense) with the expected type.

The following is a full compiling example

#include <type_traits>
#include <iostream>

struct passepartout
 {
   template <typename T>
   operator T & ();

   template <typename T>
   operator T && ();
 };

template <typename F,
   std::enable_if_t<
           std::is_invocable_v<F, passepartout &>
      && ! std::is_invocable_v<F, passepartout &, passepartout &>, bool>
         = true>
void higherOrderFunc (F)
 { std::cout << "-- one parameter callable" << std::endl; }

template <typename F,
   std::enable_if_t<
      std::is_invocable_v<F, passepartout &, passepartout &>, bool> = true>
void higherOrderFunc (F)
 { std::cout << "-- two parameter callable" << std::endl; }

int main ()
 {
   auto l1a = [](auto &&){};
   auto l1b = [](int &){};
   auto l2a = [](auto &, int &&){};
   auto l2b = [](auto, int const &){};
   auto l2c = [](auto &&, auto const &){};
   auto l2d = [](int &&, auto const &, auto && ...){};

   higherOrderFunc(l1a);
   higherOrderFunc(l1b);
   higherOrderFunc(l2a);
   higherOrderFunc(l2b);
   higherOrderFunc(l2c);
   higherOrderFunc(l2c);
   higherOrderFunc(l2d);
 }

Upvotes: 3

Related Questions