Count the number of arguments in a lambda
Asked Answered
G

3

15

I need to know the exact number of arguments that a lambda has. I do not care for their types, I just need a count.

auto lambda0 = [&]() { ... };
auto lambda1 = [&](int32_t a) { ... };
auto lambda2 = [&](int32_t a, auto b) { ... };

lambda_details<decltype(lambda0)>::argument_count; // Equals 0
lambda_details<decltype(lambda1)>::argument_count; // Equals 1
lambda_details<decltype(lambda2)>::argument_count; // Equals 2

Detecting variadic lambdas would also be nice so that I can deal with that edge case as well.

auto lambda_variadic = [&](auto... args){ ... };

lambda_details<decltype(lambda_variadic)>::is_variadic; // Equals true

How can I get this information?

Gallium answered 27/1, 2019 at 15:38 Comment(2)
Something like this works for non-variadic, non-generic lambda (or generally, any class with exactly one non-templated overload of operator()). Off the top, I'm not sure how to extend it to other cases.Gerge
Definitely check out this talk by Antony Polukhin. It shows how you can not only deduce the number of arguments but also their types.Dulcet
G
4

I have solved it using a modified version of @yuri kilochek's answer.

Instead of starting from 50 arguments and counting down, we start at zero and count up. When we get a match we know the minimum amount of arguments required to call the lambda. We then keep searching up until a sane maximum to see if there is a maximum amount of arguments (this can happen when you have default arguments).

If the argument count limit is reached, we assume the lambda to be variadic.

This implementation reduces the amount of template instantiations for non variadic lambdas significantly. It also gives us the minimum amount of arguments for all lambdas, and the maximum amount of arguments for any non-variadic lambdas.

Again, big thanks to Yuri Kilochek for laying the foundation for this elegant solution. Check his answer for more details about the implementation.

struct any_argument
{
    template <typename T>
    operator T && () const;
};

template <typename Lambda, typename Is, typename = void>
struct can_accept_impl : std::false_type
{};

template <typename Lambda, std::size_t ...Is>
struct can_accept_impl <Lambda, std::index_sequence<Is...>, decltype(std::declval<Lambda>()(((void)Is, any_argument{})...), void())> : std::true_type
{};

template <typename Lambda, std::size_t N>
struct can_accept : can_accept_impl<Lambda, std::make_index_sequence<N>>
{};

template <typename Lambda, std::size_t N, size_t Max, typename = void>
struct lambda_details_maximum
{
    static constexpr size_t maximum_argument_count = N - 1;
    static constexpr bool is_variadic = false;
};

template <typename Lambda, std::size_t N, size_t Max>
struct lambda_details_maximum<Lambda, N, Max, std::enable_if_t<can_accept<Lambda, N>::value && (N <= Max)>> : lambda_details_maximum<Lambda, N + 1, Max>
{};

template <typename Lambda, std::size_t N, size_t Max>
struct lambda_details_maximum<Lambda, N, Max, std::enable_if_t<can_accept<Lambda, N>::value && (N > Max)>>
{
    static constexpr bool is_variadic = true;
};

template <typename Lambda, std::size_t N, size_t Max, typename = void>
struct lambda_details_minimum : lambda_details_minimum<Lambda, N + 1, Max>
{
    static_assert(N <= Max, "Argument limit reached");
};

template <typename Lambda, std::size_t N, size_t Max>
struct lambda_details_minimum<Lambda, N, Max, std::enable_if_t<can_accept<Lambda, N>::value>> : lambda_details_maximum<Lambda, N, Max>
{
    static constexpr size_t minimum_argument_count = N;
};

template <typename Lambda, size_t Max = 50>
struct lambda_details : lambda_details_minimum<Lambda, 0, Max>
{};

Another important thing to note is that any_argument doesn't automatically play nice with operators. You will have to overload every single one if you want it to work with auto arguments that are operated upon (e.g. [](auto a) { return a * 2; }). It will end up looking more like this:

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

    any_argument& operator ++();
    any_argument& operator ++(int);
    any_argument& operator --();
    any_argument& operator --(int);

    template <typename T> friend any_argument operator + (const any_argument&, const T&);
    template <typename T> friend any_argument operator + (const T&, const any_argument&);
    template <typename T> friend any_argument operator - (const any_argument&, const T&);
    template <typename T> friend any_argument operator - (const T&, const any_argument&);
    template <typename T> friend any_argument operator * (const any_argument&, const T&);
    template <typename T> friend any_argument operator * (const T&, const any_argument&);
    template <typename T> friend any_argument operator / (const any_argument&, const T&);
    template <typename T> friend any_argument operator / (const T&, const any_argument&);

    // And every other operator in existence
};
Gallium answered 28/1, 2019 at 0:23 Comment(4)
Accept the answer then. Also your approach would incorrectly report lambda with default arguments as variadic.Phane
@yurikilochek Ugh I forgot about default arguments -.- . I suppose you could keep checking to see if the invocations ever stop working, but then you end up with a magic "max" number again. I'll have to think on it some more. Also, I can't accept my own answer within the first few days of posting the question.Gallium
@yurikilochek I have updated my answer to support default arguments as well.Gallium
This doesn't seem to work for reference types, e.g. auto lda = [](auto& x)->void{}Anticlimax
P
8

You can create an object that can go into any parameter by overloading conversion operator. From there just test if the lambda is callable with a given number of such arguments, counting down from some arbitrary large number. If the lambda happens to be callable on the first try (with given arbitrary large number of arguments), we can assume it is variadic:

#include <iostream>
#include <utility>
#include <type_traits>


struct any_argument {
    template <typename T>
    operator T&&() const;
};


template <typename Lambda, typename Is, typename = void>
struct can_accept_impl
: std::false_type
{};

template <typename Lambda, std::size_t ...Is>
struct can_accept_impl<Lambda, std::index_sequence<Is...>, 
                       decltype(std::declval<Lambda>()(((void)Is, any_argument{})...), void())>
: std::true_type
{};

template <typename Lambda, std::size_t N>
struct can_accept
: can_accept_impl<Lambda, std::make_index_sequence<N>>
{};


template <typename Lambda, std::size_t Max, std::size_t N, typename = void>
struct lambda_details_impl
: lambda_details_impl<Lambda, Max, N - 1>
{};

template <typename Lambda, std::size_t Max, std::size_t N>
struct lambda_details_impl<Lambda, Max, N, std::enable_if_t<can_accept<Lambda, N>::value>>
{
    static constexpr bool is_variadic = (N == Max);
    static constexpr std::size_t argument_count = N;
};

template <typename Lambda, std::size_t Max = 50>
struct lambda_details
: lambda_details_impl<Lambda, Max, Max>
{};


int main()
{
    auto lambda0 = []() {};
    auto lambda1 = [](int a) {};
    auto lambda2 = [](int a, auto b) {};
    auto lambda3 = [](int a, auto b, char = 'a') {};
    auto lambda4 = [](int a, auto b, char = 'a', auto...) {};

    std::cout << lambda_details<decltype(lambda0)>::is_variadic << " " << lambda_details<decltype(lambda0)>::argument_count << "\n"; // 0 0
    std::cout << lambda_details<decltype(lambda1)>::is_variadic << " " << lambda_details<decltype(lambda1)>::argument_count << "\n"; // 0 1
    std::cout << lambda_details<decltype(lambda2)>::is_variadic << " " << lambda_details<decltype(lambda2)>::argument_count << "\n"; // 0 2
    std::cout << lambda_details<decltype(lambda3)>::is_variadic << " " << lambda_details<decltype(lambda3)>::argument_count << "\n"; // 0 3
    std::cout << lambda_details<decltype(lambda4)>::is_variadic << " " << lambda_details<decltype(lambda4)>::argument_count << "\n"; // 1 50
}
Phane answered 27/1, 2019 at 18:52 Comment(1)
Great idea that any_argument; I have to remember it.Menke
G
4

I have solved it using a modified version of @yuri kilochek's answer.

Instead of starting from 50 arguments and counting down, we start at zero and count up. When we get a match we know the minimum amount of arguments required to call the lambda. We then keep searching up until a sane maximum to see if there is a maximum amount of arguments (this can happen when you have default arguments).

If the argument count limit is reached, we assume the lambda to be variadic.

This implementation reduces the amount of template instantiations for non variadic lambdas significantly. It also gives us the minimum amount of arguments for all lambdas, and the maximum amount of arguments for any non-variadic lambdas.

Again, big thanks to Yuri Kilochek for laying the foundation for this elegant solution. Check his answer for more details about the implementation.

struct any_argument
{
    template <typename T>
    operator T && () const;
};

template <typename Lambda, typename Is, typename = void>
struct can_accept_impl : std::false_type
{};

template <typename Lambda, std::size_t ...Is>
struct can_accept_impl <Lambda, std::index_sequence<Is...>, decltype(std::declval<Lambda>()(((void)Is, any_argument{})...), void())> : std::true_type
{};

template <typename Lambda, std::size_t N>
struct can_accept : can_accept_impl<Lambda, std::make_index_sequence<N>>
{};

template <typename Lambda, std::size_t N, size_t Max, typename = void>
struct lambda_details_maximum
{
    static constexpr size_t maximum_argument_count = N - 1;
    static constexpr bool is_variadic = false;
};

template <typename Lambda, std::size_t N, size_t Max>
struct lambda_details_maximum<Lambda, N, Max, std::enable_if_t<can_accept<Lambda, N>::value && (N <= Max)>> : lambda_details_maximum<Lambda, N + 1, Max>
{};

template <typename Lambda, std::size_t N, size_t Max>
struct lambda_details_maximum<Lambda, N, Max, std::enable_if_t<can_accept<Lambda, N>::value && (N > Max)>>
{
    static constexpr bool is_variadic = true;
};

template <typename Lambda, std::size_t N, size_t Max, typename = void>
struct lambda_details_minimum : lambda_details_minimum<Lambda, N + 1, Max>
{
    static_assert(N <= Max, "Argument limit reached");
};

template <typename Lambda, std::size_t N, size_t Max>
struct lambda_details_minimum<Lambda, N, Max, std::enable_if_t<can_accept<Lambda, N>::value>> : lambda_details_maximum<Lambda, N, Max>
{
    static constexpr size_t minimum_argument_count = N;
};

template <typename Lambda, size_t Max = 50>
struct lambda_details : lambda_details_minimum<Lambda, 0, Max>
{};

Another important thing to note is that any_argument doesn't automatically play nice with operators. You will have to overload every single one if you want it to work with auto arguments that are operated upon (e.g. [](auto a) { return a * 2; }). It will end up looking more like this:

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

    any_argument& operator ++();
    any_argument& operator ++(int);
    any_argument& operator --();
    any_argument& operator --(int);

    template <typename T> friend any_argument operator + (const any_argument&, const T&);
    template <typename T> friend any_argument operator + (const T&, const any_argument&);
    template <typename T> friend any_argument operator - (const any_argument&, const T&);
    template <typename T> friend any_argument operator - (const T&, const any_argument&);
    template <typename T> friend any_argument operator * (const any_argument&, const T&);
    template <typename T> friend any_argument operator * (const T&, const any_argument&);
    template <typename T> friend any_argument operator / (const any_argument&, const T&);
    template <typename T> friend any_argument operator / (const T&, const any_argument&);

    // And every other operator in existence
};
Gallium answered 28/1, 2019 at 0:23 Comment(4)
Accept the answer then. Also your approach would incorrectly report lambda with default arguments as variadic.Phane
@yurikilochek Ugh I forgot about default arguments -.- . I suppose you could keep checking to see if the invocations ever stop working, but then you end up with a magic "max" number again. I'll have to think on it some more. Also, I can't accept my own answer within the first few days of posting the question.Gallium
@yurikilochek I have updated my answer to support default arguments as well.Gallium
This doesn't seem to work for reference types, e.g. auto lda = [](auto& x)->void{}Anticlimax
M
2

I don't know a way to count all argument of a generic-lambda [edit: but yuri kilochek know how to do it: see his answer for a great solution].

For non-generic lambdas, as suggested by Igor Tandetnik, you can detect the types (return and arguments) of the pointer to operator() and count the arguments.

Something as follows

// count arguments helper
template <typename R, typename T, typename ... Args>
constexpr std::size_t  cah (R(T::*)(Args...) const)
 { return sizeof...(Args); }

// count arguments helper
template <typename R, typename T, typename ... Args>
constexpr std::size_t  cah (R(T::*)(Args...))
 { return sizeof...(Args); }

template <typename L>
constexpr auto countArguments (L)
 { return cah(&L::operator()); }

But, unfortunately, this doesn't works when you introduce an auto argument because, with an auto argument, you transform operator() in a template function.

About detecting a variadic lambda, you can detect a function with only a variadic list of arguments (let me call it "pure variadic"), as your lambda_variadic, trying to call it with zero and with (by example) 50 argument of a given type.

I mean something as follows

template <typename T, std::size_t>
struct getType
 { using type = T; };

template <typename T, std::size_t N>
using getType_t = typename getType<T, N>::type;

// isPureVariadic arguments helper
template <typename T>
constexpr std::false_type ipvh (...);

// isPureVariadic arguments helper
template <typename T, typename F, std::size_t ... Is>
constexpr auto ipvh (F f, std::index_sequence<Is...>)
   -> decltype( f(std::declval<getType_t<T, Is>>()...), std::true_type{} );

template <typename F>
constexpr bool isPureVariadic (F f)
 { return
      decltype(ipvh<int>(f, std::make_index_sequence<0u>{}))::value
   && decltype(ipvh<int>(f, std::make_index_sequence<50u>{}))::value; }

but this isn't perfect because gives false positives and false negatives.

A problem is that when you check it with a "not pure variadic lambda" as

 auto lambda_variadic2 = [&](std::string, auto... args){ ... };

that is variadic but the first argument doesn't accept a int, isn't detected as "pure variadic"; unfortunately the following lambda

 auto lambda_variadic3 = [&](long, auto... args){ ... };

is detected as "pure variadic" because the first argument accept a int.

To avoid this problem, you can modify the function to check the call with 50 arguments of two incompatible types; by example

template <typename F>
constexpr bool isPureVariadic (F f)
 { return
      decltype(ipvh<int>(f, std::make_index_sequence<0u>{}))::value
   && decltype(ipvh<int>(f, std::make_index_sequence<50u>{}))::value
   && decltype(ipvh<std::string>(f, std::make_index_sequence<50u>{}))::value; }

Another problem is that are detected as "pure virtual" also non-variadic generic-lambda functions receiving a number of arguments higher that the checked number (50, in the example).

And remain the problem that this solution doesn't detect lambda_variadic2 (a non-pure variadic lambda) as variadic.

The following is a full compiling example with the best I can imagine about your question

#include <iostream>
#include <utility>
#include <type_traits>

// count arguments helper
template <typename R, typename T, typename ... Args>
constexpr std::size_t  cah (R(T::*)(Args...) const)
 { return sizeof...(Args); }

// count arguments helper
template <typename R, typename T, typename ... Args>
constexpr std::size_t  cah (R(T::*)(Args...))
 { return sizeof...(Args); }

template <typename L>
constexpr auto countArguments (L)
 { return cah(&L::operator()); }

template <typename T, std::size_t>
struct getType
 { using type = T; };

template <typename T, std::size_t N>
using getType_t = typename getType<T, N>::type;

// isPureVariadic arguments helper
template <typename T>
constexpr std::false_type ipvh (...);

// isPureVariadic arguments helper
template <typename T, typename F, std::size_t ... Is>
constexpr auto ipvh (F f, std::index_sequence<Is...>)
   -> decltype( f(std::declval<getType_t<T, Is>>()...), std::true_type{} );

template <typename F>
constexpr bool isPureVariadic (F f)
 { return
      decltype(ipvh<int>(f, std::make_index_sequence<0u>{}))::value
   && decltype(ipvh<int>(f, std::make_index_sequence<50u>{}))::value; }


int main() {
   auto lambda0 = [&]() {};
   auto lambda1 = [&](int) {};
   auto lambda2 = [&](int, auto) {};
   auto lambda3 = [&](auto...) {};

   std::cout << countArguments(lambda0) << std::endl;
   std::cout << countArguments(lambda1) << std::endl;
   // std::cout << countArguments(lambda2) << std::endl; // compilation error
   // std::cout << countArguments(lambda3) << std::endl; // compilation error

   std::cout << isPureVariadic(lambda0) << std::endl;
   std::cout << isPureVariadic(lambda1) << std::endl;
   std::cout << isPureVariadic(lambda2) << std::endl;
   std::cout << isPureVariadic(lambda3) << std::endl;
}
Menke answered 27/1, 2019 at 16:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.