Шаблон функции принимает и возвращает разные лямбды

У меня есть функция GetThing следующим образом:

auto GetThing(size_t index, auto&& l1)
{
    return l1;
}
auto GetThing(size_t index, auto&& l1, auto&&... rest)
{
    if (index == 0)
        return l1;
    return GetThing(index - 1, rest...);
}

Я хочу, чтобы он также мог работать с разными лямбда-выражениями, в то же время имея возможность обрабатывать другие типы (имеются в виду не лямбда-выражения, не функции, такие как int и...) , например

std::cout << GetThing(1, 2, 3, 4);   //works, return 3
std::cout << GetThing(1, [] {return 0; }, 
    [] {return 1; }, [] {return 2; }, 
    [] {return 3; } )();             //nope

Но проблема здесь в том, что лямбда-выражения имеют другой тип, поэтому рекурсивная функция будет выводить несовместимый тип возвращаемого значения, поэтому мне, кажется, придется использовать std::function вот так, но это уродливо.

std::cout << GetThing(1, std::function{ [] {return 0; } }, std::function{ [] {return 1; } }, std::function{ [] {return 2; } }, std::function{ [] {return 3; } })();//works

Любой возможный способ обойти это, например, если есть перегруженный operator(), он автоматически применяет тип std::function?

РЕДАКТИРОВАТЬ: я знаю, что лямбда-выражения без захвата могут быть преобразованы в указатель функции, но как вывести это таким образом без std::decay в шаблоне? Потому что я все еще хочу обрабатывать другие типы как ссылки

EDIT2: я получаю несколько ответов, используя std::variant, и я думаю о том, что, помимо лямбда, типы параметров должны быть одинаковыми, например. std::variant<int, int, int>. Возможно, можно добавить перегрузку к GetThing, чтобы, когда std::variant содержит те же типы, он возвращал объект этого типа, в противном случае (в случае получения лямбда-выражений) возвращал std::function


person sz ppeter    schedule 03.08.2020    source источник
comment
что такое GetLambda? опечатка? Можете ли вы опубликовать минимально воспроизводимый пример?   -  person 463035818_is_not_a_number    schedule 03.08.2020
comment
@idclev463035818 Извините, это опечатка, я исправил   -  person sz ppeter    schedule 03.08.2020
comment
Лямбды ничего не захватывают? Тогда вы можете использовать указатель функции вместо std::function.   -  person songyuanyao    schedule 03.08.2020
comment
Ваши лямбда-выражения не всегда без захвата? Затем вы можете сделать GetThing(1, +[]{return 0;}, ...) (примечание +), что не так уродливо, как std::function.   -  person HolyBlackCat    schedule 03.08.2020
comment
@szppeter Я все еще хочу обрабатывать другие типы... Вы имеете в виду лямбды, принимающие разные типы параметров или возвращающие другой тип?   -  person songyuanyao    schedule 03.08.2020
comment
Кстати, вашу функцию можно заменить на std::array{a, b, c}[i]. Даже если вы хотите сохранить функцию, я предлагаю избавиться от рекурсии.   -  person HolyBlackCat    schedule 03.08.2020
comment
@HolyBlackCat Да, я знаю об этом. Это упрощенный пример моих функций, которые должны обрабатывать некоторые условия на основе некоторых параметров.   -  person sz ppeter    schedule 03.08.2020


Ответы (4)


Вы можете хранить свои функции в массиве вариантов. Конечно, это связано с некоторыми накладными расходами. Но это позволяет иметь функции, также использующие захваченные переменные.

Это позволяет выбрать функцию из такого набора функций и выполнить ее с заданными параметрами следующим образом:

template < typename ARR_T >
struct Collect
{
    template < typename ... T > 
    Collect( T&&...args  ): arr{std::forward<T>(args)...}{}
    ARR_T arr;
    using VARIANT_T = ARR_T::value_type;
    VARIANT_T& operator[]( size_t index) { return arr[index]; }
};

template < typename ... T > 
Collect( T&& ... args ) -> Collect< std::array< std::variant<T... >, sizeof...(T) >>; 

template < typename C, typename ... PARMS >
auto GetThing( size_t index, C&& c, PARMS&&... parms ) 
{
    return std::visit( [ &parms...]( auto&& func)
                      {
                          return func(std::forward<PARMS>(parms)...);
                      }, c[index]);
}

int main()
{
    std::cout << GetThing( 2, Collect(  []( int, double) {return 0; }, []( int, double) {return 1; }, []( int, double) {return 2; }, []( int, double) {return 3; }), 1,5.6)<< std::endl;

    int y = 8;
    double d = 9.99;

    std::cout << GetThing( 0, Collect(  [y,d]( int, double) {return d*y; }, []( int, double) {return 1.; }, []( int, double) {return 2.; }, []( int, double) {return 3.; }), 1,5.6)<< std::endl;
}



В этом случае GetThing также принимает параметры функции для вызова лямбды, потому что вызов использует std::visit. Если вы хотите выбрать только функцию, вы получите std::variant, если хотите, и сможете вызвать функцию самостоятельно.


    auto func = Collect(  []( int i, double d) {return d+i; }, []( int i, double d) {return d*i; }, []( int i, double d) {return d-i; } )[2];
    std::cout << std::visit( []( auto&& f) { return f( 9, 7.77 ); }, func ) << std::endl;
}
person Klaus    schedule 03.08.2020
comment
Проголосовал, очень интересное решение. Но по-прежнему не может обрабатывать другие нормальные типы, отличные от лямбда/не функций. - person sz ppeter; 04.08.2020
comment
@szppeter Какой обработки вы хотите достичь. Если вещь не исполняемая, что вы хотите с ней делать? Класс collect собирает все, так что проблем нет. Но мой GetThing вызывает/выполняет этот объект, как мне казалось, и было идеей. Что вы хотите делать с нефункциональными не лямбда-типами? - person Klaus; 04.08.2020
comment
Например, возврат n-го целого числа в списке параметров, который не включает вызов функции, поэтому функция GetThing не GetLambda или какая-либо конкретная. Я просто хочу, чтобы он возвращал n-й параметр (рядом с index) - person sz ppeter; 04.08.2020
comment
Проще говоря, мне просто нужна функция GetThing, которую также можно использовать для получения лямбда (которую можно преобразовать в std::function или указатель функции или что-то еще, что работает), если это когда-либо возможно - person sz ppeter; 04.08.2020
comment
@szppeter Это уже сделано. Класс Collect точно доставляет любой введенный вами тип, инкапсулированный в вариант. Я не вижу твоей проблемы, потому что вижу, что она уже решена. Можете ли вы добавить псевдокод к своему вопросу: 1) сохранить данные в коллекцию, 2) выбрать какой-либо элемент, 3) сделать что-то с найденным элементом. Особенно последний пункт полностью не определен. Класс сбора уже собирает все данные, выбирает один элемент и возвращает его. Так чего не хватает? - person Klaus; 04.08.2020
comment
ОК, я отредактировал свой вопрос. В ваших ответах предполагается, что параметр (в данном случае c) будет равен Collection, что не удастся для простого случая, например GetThing(1, 2, 3, 4);. - person sz ppeter; 04.08.2020
comment
@szppeter Возьмите Collect(1,2,3,4)[2] и вы получите std::variant‹...› с int и значением 3. - person Klaus; 04.08.2020
comment
Это не удается, если вы передаете значения, отличные от r, в Collect, так как это приводит к варианту ссылок, что является незаконным. Он также создает N вариантов в массиве, некоторые из которых получают копии аргументов, что кажется неэффективным; он использует память max(k+sizeof(Ts)...), умноженную на sizeof...(Ts) в автоматическом хранилище, и я сомневаюсь, что большинство оптимизаторов смогут этого избежать. - person Yakk - Adam Nevraumont; 04.08.2020
comment
Мое улучшение, которое унифицирует интерфейс GetThing() относительно моего EDIT2 - person sz ppeter; 04.08.2020

Вы можете вернуть std::variant, который содержит все типы ввода:

template <typename... Args>
std::variant<std::decay_t<Args>...>
GetThing(std::size_t index, Args&&... args)
{ 
  return [index, t=std::forward_as_tuple(std::forward<Args>(args)...)] 
    <std::size_t... Is>(std::index_sequence<Is...>) { 
    return std::array{ +[](const std::tuple<Args&&...>& t) { 
      return std::variant<std::decay_t<Args>...>{ 
        std::in_place_index<Is>, std::get<Is>(t)}; 
      } ... 
    }[index](t); 
  }(std::index_sequence_for<Args...>{}); 
}

Затем вам нужно std::visit посетить возвращаемое значение:

for (std::size_t index = 0; index < 4; index++)
  std::visit(
    [](auto&& f) { std::cout << f() << " "; }, 
    GetThing(index, []{return 0;}, []{return 1;}, []{return 2;}, []{return 3;})
  );
person 康桓瑋    schedule 04.08.2020
comment
Это работает, только если индекс известен во время компиляции. Это легкая сторона :-) - person Klaus; 04.08.2020
comment
Если индекс известен во время компиляции, вы также можете использовать std::get и кортеж. - person HolyBlackCat; 04.08.2020
comment
Проголосовал, я думаю, что это больше похоже на упрощенную версию ответа Клауса - person sz ppeter; 04.08.2020
comment
Это можно очистить дальше; на самом деле, рекурсия может быть устранена. - person Yakk - Adam Nevraumont; 04.08.2020
comment
@Yakk-AdamNevraumont Рекурсия какой функции? - person 康桓瑋; 04.08.2020
comment
@康桓瑋 GetThing, как написано выше, является рекурсивным, как и Get? - person Yakk - Adam Nevraumont; 04.08.2020
comment
@Yakk-AdamNevraumont Вы имеете в виду использование std::index_sequence для устранения рекурсии обеих функций? - person 康桓瑋; 04.08.2020
comment
@康桓瑋 Конечно, это один из способов. Я просто с подозрением отношусь к рекурсии O(N) в моем программировании шаблонов, потому что я считаю, что это увеличивает время компиляции и использование памяти даже при скромном N. В вашем случае вы генерируете общую длину символа O(N^2) и O (N^2) код для всех тел. Тела можно оптимизировать, но это требует времени и памяти во время компиляции. А компиляторы шаблонов, по моему опыту, не очень эффективны. - person Yakk - Adam Nevraumont; 04.08.2020

Для лямбд без захвата вы можете использовать указатели функций

#include <utility> // std::forward

auto GetThing(size_t index, auto&& l1)
{
    return  std::forward<decltype(l1)>(l1);
}

auto GetThing(size_t index, auto&& l1, auto&&... rest)
{
    if (index == 0)
        return std::forward<decltype(l1)>(l1);
    return GetThing(index - 1, std::forward<decltype(rest)>(rest)...);
}

std::cout << GetThing(1,
+[] {return 0; }, +[] {return 1; }, 
+[] {return 2; }, +[] {return 3; } 
)();// works now

будет работать: Демо

Также обратите внимание, что вам нужно добавить + для преобразования лямбда в указатель функции. Подробнее: Положительная лямбда: '+[]{}' - Что такое колдовство это?


Кроме того, в случае лямбды с захватом вам нужно использовать std::function. Вышеупомянутого будет недостаточно/не будет работать!

person Const    schedule 03.08.2020
comment
Вы неправильно используете std::forward. (Параметры должны быть ссылками для пересылки и, возможно, типом возвращаемого значения.) - person HolyBlackCat; 03.08.2020
comment
@HolyBlackCat Извините, я забыл. Я исправил это. Спасибо за исправление. - person Const; 03.08.2020
comment
Вы также можете упомянуть, что это + заставляет его работать. Добавление std::forward — это просто общее улучшение. - person HolyBlackCat; 03.08.2020
comment
@HolyBlackCat Да, добавлено... Спасибо - person Const; 03.08.2020
comment
В качестве небольшого улучшения вы можете сделать указатель на функцию распада внутри GetThing, и тогда место вызова будет таким же, как у OP. демонстрация. - person cigien; 03.08.2020

Как решил @康桓瑋, вариант - это то, что вам нужно, когда у вас есть тип суммы.

Вот что я считаю более чистым решением.

template<std::size_t...Is>
using enumeration = std::variant<std::integral_constant<std::size_t, Is>...>;

enumeration — это вариант значений времени компиляции.

Они представляют собой хранилище времени выполнения для значения времени компиляции, которое затем можно использовать.

template<class T>
struct enum_from_seq;
template<class T>
using enum_from_seq_t = typename enum_from_seq<T>::type;
template<std::size_t...Is>
struct enum_from_seq<std::index_sequence<Is...>>{ using type=enumeration<Is...>; };

template<std::size_t N>
using variant_index_t = enum_from_seq_t<std::make_index_sequence<N>>;

variant_index_t — это enumeration от 0 до N-1. Его можно использовать для взаимодействия с вариантами.

template<std::size_t N, std::size_t...Is>
variant_index_t<N> make_variant_index_helper( std::size_t I, std::index_sequence<Is...> ) {
  constexpr variant_index_t<N> retvals[] = {
    variant_index_t<N>{ std::integral_constant<std::size_t, Is>{} }...
  };
  return retvals[I];
}

template<std::size_t N>
variant_index_t<N> make_variant_index( std::size_t I ) {
  return make_variant_index_helper<N>( I, std::make_index_sequence<N>{} );
}
//TODO: handle valueless_by_exception:
template<class...Ts>
variant_index_t<sizeof...(Ts)> get_index( std::variant<Ts...>const& v ) {
  return make_variant_index<sizeof...(Ts)>( v.index() );
}

теперь мы можем сделать enumeration для варианта из размера и элемента времени выполнения.

Это раздражает механизм, но, по крайней мере, он общий.

template<class...Ts>
std::variant< std::decay_t<Ts>... > pick( std::size_t I, Ts&&...ts ) {
  using return_type = std::variant< std::decay_t<Ts>... >;

  std::tuple< std::remove_reference_t<Ts>*... > retvals = {std::addressof(ts)...};

  auto index = make_variant_index<sizeof...(Ts)>(I);
        
  return std::visit( [&](auto I)->return_type {
    //TODO: perfect forward here.  If constexpr on the Ith Ts maybe?
    return return_type(std::in_place_index<I>, *std::get<I>(retvals));
  }, index );
}

видите, нет рекурсии.

Тестовый код:

for (std::size_t i = 0; i < 5; ++i)
{
    auto x = pick( i, 3.14, 2, "hello", "goodbye", -3.14 );
    std::visit( [](auto&& x) {
        std::cout << x << "\n";
    }, x );
}

Живой пример.

Выход:

3.14
2
hello
goodbye
-3.14
person Yakk - Adam Nevraumont    schedule 04.08.2020