Arity of a generic lambda
Asked Answered
W

5

11

It is possible to deduce arity of a non-generic lambda by accessing its operator().

template <typename F>
struct fInfo : fInfo<decltype(&F::operator())> { };

template <typename F, typename Ret, typename... Args>
struct fInfo<Ret(F::*)(Args...)const> { static const int arity = sizeof...(Args); };

This is nice and dandy for something like [](int x){ return x; } as the operator() is not templated.

However, generic lambdas do template the operator() and it is only possible to access a concrete instantiation of the template - which is slightly problematic because I can't manually provide template arguments for the operator() as I don't know what its arity is.

So, of course, something like

auto lambda = [](auto x){ return x; };
auto arity = fInfo<decltype(lambda)>::arity;

doesn't work.

I don't know what to cast to nor do I know what template arguments to provide (or how many) (operator()<??>).
Any ideas how to do this?

Waves answered 6/9, 2014 at 19:54 Comment(2)
The operator() of generic lambdas can be variadic templates. What's your definition of arity for those?Richella
Why do you want to ask arity, and not 'how many ints can you accept?'?Outsail
P
3

It's impossible, as the function call operator can be a variadic template. It's been impossible to do this forever for function objects in general, and special-casing lambdas because they happened to not be equally powerful was always going to be a bad idea. Now it's just time for that bad idea to come home to roost.

Periodical answered 6/9, 2014 at 20:19 Comment(0)
O
6

This technique will work in some cases. I create a fake_anything type that can fake almost anything, and try to invoke your lambda with some number of instances of that.

#include <iostream>

struct fake_anything {
  fake_anything(fake_anything const&);
  fake_anything();
  fake_anything&operator=(fake_anything const&);
  template<class T>operator T&() const;
  template<class T>operator T&&() const;
  template<class T>operator T const&() const;
  template<class T>operator T const&&() const;
  fake_anything operator*() const;
  fake_anything operator++() const;
  fake_anything operator++(int) const;
  fake_anything operator->() const;
  template<class T>fake_anything(T&&);
};
fake_anything operator+(fake_anything, fake_anything);
fake_anything operator-(fake_anything, fake_anything);
fake_anything operator*(fake_anything, fake_anything);
fake_anything operator/(fake_anything, fake_anything);
// etc for every operator

template<class>using void_t=void;
template<class Sig, class=void>
struct can_invoke:std::false_type{};
template<class F, class...Args>
struct can_invoke<F(Args...),
  void_t< decltype( std::declval<F>()( std::declval<Args>()... ) ) >
> : std::true_type
{};

template<class Sig>struct is_sig:std::false_type{};
template<class R, class...Args>struct is_sig<R(Args...)>:std::true_type{};

template<unsigned...>struct indexes{using type=indexes;};
template<unsigned Max,unsigned...Is>struct make_indexes:make_indexes<Max-1,Max-1,Is...>{};
template<unsigned...Is>struct make_indexes<0,Is...>:indexes<Is...>{};
template<unsigned max>using make_indexes_t=typename make_indexes<max>::type;

template<class T,unsigned>using unpacker=T;

template<class F, class A, class indexes>
struct nary_help;
template<class F, class A, unsigned...Is>
struct nary_help<F,A,indexes<Is...>>:
  can_invoke<F( unpacker<A,Is>... )>
{};
template<class F, unsigned N>
struct has_n_arity:
  nary_help<F, fake_anything, make_indexes_t<N>>
{};

template<class F, unsigned Min=0, unsigned Max=10>
struct max_arity{
  enum{Mid=(Max+Min)/2};
  enum{
    lhs = max_arity<F,Min,Mid>::value,
    rhs = max_arity<F,Mid+1,Max>::value,
    value = lhs>rhs?lhs:rhs,
  };
};
template<class F, unsigned X>
struct max_arity<F,X,X>:
  std::integral_constant<int, has_n_arity<F,X>::value?(int)X:-1>
{};

template<class F, unsigned Min=0, unsigned Max=10>
struct min_arity{
  enum{Mid=(Max+Min)/2};
  enum{
    lhs = min_arity<F,Min,Mid>::value,
    rhs = min_arity<F,Mid+1,Max>::value,
    value = lhs<rhs?lhs:rhs,
  };
};
template<class F, unsigned X>
struct min_arity<F,X,X>:
  std::integral_constant<unsigned,has_n_arity<F,X>::value?X:(unsigned)-1>
{};

auto test1 = [](auto x, auto y)->bool { return x < y; };
auto test2 = [](auto x, auto y) { return x + y; };
auto test3 = [](auto x) { return x.y; };

int main() {
  std::cout << can_invoke< decltype(test1)( fake_anything, fake_anything ) >::value << "\n";
  std::cout << can_invoke< decltype(test1)( int, int ) >::value << "\n";
  std::cout << has_n_arity< decltype(test1), 2 >::value << "\n";
  std::cout << max_arity< decltype(test1) >::value << "\n";
  std::cout << max_arity< decltype(test2) >::value << "\n";
  // will fail to compile:
  // std::cout << max_arity< decltype(test3) >::value << "\n";
}

live example.

Note sufficient SFINAE will mean the above will get the wrong result, as will use of operator., or use of operator. on certain kinds of "derived" types, or accessing types based off of the fake_anything parameter, etc.

However, if the lambda specifies its return value with a ->X clause, then fake_anything is more than good enough. The hard part is dealing with the body.

Note that this approach is often a bad idea, because if you want to know the arity of a function, you probably also know the types of the things you want to invoke the function object with! And above I answer that question really easily (can this function object be invoked with these arguments?). It can even be improved to ask "what is the longest/shortest prefix of these arguments that can invoke this function object", or handle "how many repeats of type X work to invoke this function object" (if you want clean failure, you need an upper bound).

Outsail answered 6/9, 2014 at 21:8 Comment(6)
doesn't seem to work when lambda's body tries to utilize the auto parameter in a context not supported by fake_anything, e.g. return x*2; or return x.get();; because compiler evaluates the body in can_invoke<T>Acquisitive
@piotrs. ah yes, with body based type deduction it must. With -> it should have no reason to, but that still sucks. Even if I override every operator, there is still ..Outsail
@piotrs. some improvements. It works with x*2, but not x.get() if x is of type auto.Outsail
I must note that VS2015 give wrong results for most cases.Here is the output : 0 0 0 -1 2Pulpiteer
@Pulpiteer Using decltype in MSVC2015 is a recipe for disappointment.Outsail
@Yakk If you see the source of a problem, maybe you'll fill a bug report? They seems to be pretty fast at fixing them this week ;)Pulpiteer
P
3

It's impossible, as the function call operator can be a variadic template. It's been impossible to do this forever for function objects in general, and special-casing lambdas because they happened to not be equally powerful was always going to be a bad idea. Now it's just time for that bad idea to come home to roost.

Periodical answered 6/9, 2014 at 20:19 Comment(0)
C
3

This is a c++17 solution that works with generic and variadic lambdas and functors with variadic templatet operator(). the idea is to recursively simulate the call with descending number of arguments and use SFINAE to break recursion when the first matching number of arguments is found. It compilers on gcc >= 7 and Clang >=5. A working example can be found here.

#include<utility>

constexpr size_t max_arity = 10;

struct variadic_t
{
};

namespace detail
{
    // it is templated, to be able to create a
    // "sequence" of arbitrary_t's of given size and
    // hece, to 'simulate' an arbitrary function signature.
    template <size_t>
    struct arbitrary_t
    {
        // this type casts implicitly to anything,
        // thus, it can represent an arbitrary type.
        template <typename T>
        operator T &&();

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

    template <typename F, size_t... Is,
                typename U = decltype(std::declval<F>()(arbitrary_t<Is>{}...))>
    constexpr auto test_signature(std::index_sequence<Is...>)
    {
        return std::integral_constant<size_t, sizeof...(Is)>{};
    }

    template <size_t I, typename F>
    constexpr auto arity_impl(int) -> decltype(test_signature<F>(std::make_index_sequence<I>{}))
    {
        return {};
    }

    template <size_t I, typename F, typename = std::enable_if_t<(I > 0)>>
    constexpr auto arity_impl(...)
    {
        // try the int overload which will only work,
        // if F takes I-1 arguments. Otherwise this
        // overload will be selected and we'll try it 
        // with one element less.
        return arity_impl<I - 1, F>(0);
    }

    template <typename F, size_t MaxArity = 10>
    constexpr auto arity_impl()
    {
        // start checking function signatures with max_arity + 1 elements
        constexpr auto tmp = arity_impl<MaxArity + 1, F>(0);
        if constexpr (tmp == MaxArity + 1)
        {
            // if that works, F is considered variadic
            return variadic_t{};
        }
        else
        {
            // if not, tmp will be the correct arity of F
            return tmp;
        }
    }
}

template <typename F, size_t MaxArity = max_arity>
constexpr auto arity(F&& f) { return detail::arity_impl<std::decay_t<F>, MaxArity>(); }

template <typename F, size_t MaxArity = max_arity>
constexpr auto arity_v = detail::arity_impl<std::decay_t<F>, MaxArity>();

template <typename F, size_t MaxArity = max_arity>
constexpr bool is_variadic_v = std::is_same_v<std::decay_t<decltype(arity_v<F, MaxArity>)>, variadic_t>;

Usage:

auto l = [](auto...){};
static_assert(is_variadic_v<decltype(l)>);

and:

auto l = [](auto, auto, auto){};
static_assert(!is_variadic_v<decltype(l)>);
static_assert(arity(l) == 3);
Churchyard answered 22/3, 2018 at 22:4 Comment(2)
4 years later, we have a workaround that is only half as bad as the old workaround. What progress, C++!Waves
This converts the types to arbitrary_t, so if one wants to use arity_v, one has to cast those back inside the lambda.Reyna
A
2

I would say this is partially possible, at least you can know the overall arity (templated + regular types) when you explicitly instantiate the auto parameters of operator():

template <typename F, typename... Args>
struct autofInfo : fInfo<decltype(&F::template operator()<Args...>)> {};

auto lambda = [](auto x, int y, float z) { return x + y + z; };

auto arity = autofInfo<decltype(lambda), int>::arity;
//                                       ^^^ list of auto parameters instantiations

assert(3 == arity);
Acquisitive answered 6/9, 2014 at 20:28 Comment(0)
P
0

Another posible solution, for case when possible template types are known: http://coliru.stacked-crooked.com/a/e3a07d723a8f27e9

using T1 = string;
using T2 = int;

std::integral_constant<int, 1> static arity(function<void(T1)>){ return {}; }
std::integral_constant<int, 2> static arity(function<void(T1, T2)>){ return {}; }

template<class Fn>
using Arity = decltype(arity(Fn{}));
Pulpiteer answered 5/4, 2016 at 19:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.