2

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

max66
  • 65,235
  • 10
  • 71
  • 111
lightxbulb
  • 1,251
  • 12
  • 29
  • 2
    There doesn't seem to be any way to check for this. Why would you need to? – n. m. could be an AI Jul 06 '19 at 10:59
  • 2
    I'm afraid that is not possible as it would require substituting all types into the function template. You can have function templates that are only valid for some combinations of template arguments and these conditions can be arbitrarily complex. Note that only functions can be invoked, not templates. What could possibly be a use case of this? In the end, you still want to use some concrete types, so, why not check against them? – Quimby Jul 06 '19 at 11:00
  • @Quimby Assume you have a 100 types, manually writing checks for all of those is hardly practical. The whole thing has to do with implementing higher order functions. – lightxbulb Jul 06 '19 at 11:23
  • Not sure how to help you, invocability may depend on its arguments. Even `template foo(T,T)` is not valid for all types of the two arguments. And there are no restrictions on the template. The function itself may be overloaded, so C++ can only answer "if I put these concrete arguments in there, is `func(args...)` valid expression?". Both class and function templates are checked at the point of instantiation with concrete types and so the compiler can start matching available templates against these concrete types. You are asking the opposite. – Quimby Jul 06 '19 at 11:46
  • 1
    Could you perhaps give a more concrete example where a high-order function is used on something? Does not have to be compilable C++. – Quimby Jul 06 '19 at 11:49
  • @Quimby Assume you have a series of objects of different types inside the body of the higher order function, to which you want to apply `func`. So something like `func(obj)`, but for many different types of those. You can pass in a lambda with an `auto` argument type and it will be correctly instantiated for all of those. On the other hand if you want to specialize this higher order function based on the number of arguments of the lambda, you would have to write an `is_invocable` for each and every type inside. Now assume you change those types. Or for instance you generate types recursively. – lightxbulb Jul 06 '19 at 12:25
  • @Quimbly Specifically for the recursive class generation inside of the higher order function, the problem becomes pretty much infeasible even if you were willing to type out all of the is_invocable statements. – lightxbulb Jul 06 '19 at 12:26
  • There are two different statements one could imagine checking. In fantasy notation: 1. ∃ T1, T2: is_invocable(f, T1, T2). 2. ∀ T1, T2: is_invocable(f, T1, T2). Neither is expressible in C++. The second one could be expressible in a language with parametricity, but the first one seems to be rather useless in any imaginable setting. Supose the dirst statement is true, what you gonna do with this information? – n. m. could be an AI Jul 06 '19 at 17:09
  • @n.m. You would specialize based on the arity, that's what you will do. And do note that I am not asking what T1,T2 out of all types fit the function (though that should also be a valid question, considering that the compiler should have the definition of it) - I am asking whether its signature admits 2 arguments. Clearly such a concept is already in C++ to some extent, since you can do something like `[](auto x, auto y){...}`. If we are to think in terms of classes, this is like a class with a templated `operator()` that accepts two arguments - you should ideally be able to query for this. – lightxbulb Jul 06 '19 at 18:23
  • "You would specialize based on the arity" How? Here are two facts. 1. `f` is a function. 2. `f` is a function that accepts two arguments. What kind of C++ code involving `f` can you write knowing the second fact, that you cannot write knowing just the first fact? Can you show an example? "You would specialize based on the arity" This kind of amounts to creating a function that has ∀ in its signature. I'm asking about functions that have ∃ in them. There are no useful operations that can be performed on them. – n. m. could be an AI Jul 06 '19 at 19:53
  • @n.m. A very simple example (not involving templated class recursive instantiation for the most part), would be implementing a compile time map higher order function (from functional programming). Based on the arity of the function passed in, you will pass either each list value consecutively, or you will pass in the value and the index (as an integral constant). You're also conflating logic quantifiers and arity of a function. Granted a ternary function that is not defined for any three arguments makes no sense, but there's still a distinction between the two. – lightxbulb Jul 06 '19 at 20:30
  • You cannot pass anything to the function because you don't know anything about the types of its parameters. You only know there are two of them. Or do you mean something else? I have really hard time understanding what you are trying to accomplish. Don't talk about code, show it. – n. m. could be an AI Jul 06 '19 at 20:35
  • @n.m. You can pass something to it - it just won't compile if you pass the wrong things. But the function may be valid for a million types, you won't do an is_invocable for a million types (especially if recursively generated) - you want to know the arity in that case in order to specialize for the arity. Additionally if I just show code, people would focus on the specific implementation of that code, making assumptions of what one wants - my question is a bit more general. I believe I provided enough code in the question to clarify what I want. – lightxbulb Jul 06 '19 at 20:40
  • If you are interested in writing code that does not compile, there is a million easier ways accomplish that. I assume you want to compile your code; if not, please alert me so I can stop talking. "you want to know the arity" You keep saying that without showing how you would use that knowledge. Repeating it over and over again accomplishes nothing. Would you mind to **SHOW SOME CODE**? Thank you for your patience. – n. m. could be an AI Jul 06 '19 at 20:57
  • @n.m. Code that would not compile if provided wrong input, which holds more or less for all compile time things. I think it's clear we're talking past each other though, so I'll leave it at that. – lightxbulb Jul 06 '19 at 21:14

1 Answers1

3

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);
 }
max66
  • 65,235
  • 10
  • 71
  • 111
  • While this is indeed a partial solution, it has the same issue as florestan's `arbitrary_t` (https://stackoverflow.com/questions/25704173/arity-of-a-generic-lambda) where it requires a cast inside of the lambda. Try for instance `auto l2a = [](auto x, auto y) {return x+y;};`. – lightxbulb Jul 06 '19 at 13:00
  • @lightxbulb - yes... is useful only for `decltype()` like uses (as `std::is_invocable`) and (I suppose) isn't reasonable expecting something more. – max66 Jul 06 '19 at 13:24
  • @lightxbulb - maybe make sense removing the operators implementation: `decltype()` and `std::is_invocable` continue to working and the temptation of use the `passspartout` directly is reduced. But still remain the `auto &` problem. – max66 Jul 06 '19 at 13:31
  • @lightxbulb - I've modified the answer deleting the conversion operators definition and passing references to `std::is_invocable`; this heavily simplify the answer and should also solve the `auto &` problem. – max66 Jul 06 '19 at 13:48
  • Thanks for doing so. While it isn't directly applicable to my use-case (since I actually do not want to have to cast in the lambdas), it is indeed a partial solution, so I will accept it as the answer, considering that C++ currently doesn't seem provide a mechanism to achieve what I want. Hopefully in a future iteration of the standard, at some point one would even be able to query the arity and types of all overloads for a specific function (for example returned as a static array of tuples). – lightxbulb Jul 06 '19 at 14:24