Подсчитайте количество аргументов в лямбде

Мне нужно знать точное количество аргументов лямбды. Мне плевать на их типы, мне просто нужен счет.

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

Было бы неплохо обнаружить вариабельные лямбды, чтобы я мог справиться и с этим крайним случаем.

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

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

Как я могу получить эту информацию?

Ответы

Ответ 1

Вы можете создать объект, который может входить в любой параметр, перегружая оператор преобразования. Оттуда просто проверьте, может ли лямбда вызываться с заданным количеством таких аргументов, считая в обратном порядке от некоторого произвольного большого числа. Если лямбда может быть вызвана с первой попытки (с заданным произвольным большим количеством аргументов), мы можем предположить, что она является вариативной:

#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
}

Ответ 2

Я не знаю, как подсчитать все аргументы дженериковой лямбды [ править: но Юрий Килочек знает, как это сделать: см. Его ответ, чтобы найти отличное решение].

Для неуниверсальных лямбд, как предложил Игорь Тандетник, вы можете определить типы (возврат и аргументы) указателя на operator() и посчитать аргументы.

Что-то следующее

// 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()); }

Но, к сожалению, это не работает, когда вы вводите auto аргумент, потому что с auto аргументом вы преобразует operator() в функцию шаблона.

Об обнаружении лямбды с переменными значениями вы можете обнаружить функцию только с переменным списком аргументов (позвольте мне назвать ее "чисто переменным") как ваш lambda_variadic, пытаясь вызвать ее с нулем и с (например, 50) аргументом данного тип.

Я имею в виду что-то следующее

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; }

но это не идеально, потому что дает ложные срабатывания и ложные отрицания.

Проблема в том, что когда вы проверяете это с "не чистой вариабельной лямбда", как

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

это variadic, но первый аргумент не принимает int, не определяется как "pure variadic"; к сожалению следующая лямбда

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

определяется как "чистый variadic", потому что первый аргумент принимает int.

Чтобы избежать этой проблемы, вы можете изменить функцию, чтобы проверить вызов с 50 аргументами двух несовместимых типов; по примеру

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; }

Другая проблема заключается в том, что они определяются как "чисто виртуальные", а также не переменные лямбда-функции общего назначения, получающие число аргументов, превышающее проверяемое число (в примере 50).

И остается проблема, заключающаяся в том, что это решение не определяет lambda_variadic2 (не чистая вариадическая лямбда) как вариадическую.

Ниже приведен полный пример компиляции с лучшим, что я могу представить по вашему вопросу

#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;
}

Ответ 3

Я решил это, используя модифицированную версию ответа @yuri kilochek.

Вместо того, чтобы начинать с 50 аргументов и считать вниз, мы начинаем с нуля и считаем. Когда мы получаем совпадение, мы знаем минимальное количество аргументов, необходимых для вызова лямбды. Затем мы продолжаем поиск до нормального максимума, чтобы увидеть, есть ли максимальное количество аргументов (это может случиться, когда у вас есть аргументы по умолчанию).

Если предел количества аргументов достигнут, мы предполагаем, что лямбда-переменная.

Эта реализация значительно сокращает количество экземпляров шаблона для невариантных лямбд. Это также дает нам минимальное количество аргументов для всех лямбд и максимальное количество аргументов для любых невариантных лямбд.

Еще раз большое спасибо Юрию Килочек за то, что он заложил основу для этого элегантного решения. Проверьте его ответ для более подробной информации о реализации.

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>
{};

Еще одна важная вещь, которую стоит отметить, это то, что any_argument автоматически не работает с операторами. Вам придется перегружать каждый из них, если вы хотите, чтобы он работал с auto аргументами, с которыми оперируют (например, [](auto a) { return a * 2; }). В конечном итоге это будет выглядеть примерно так:

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
};