Function template accept and return different lambdas

Viewed 345

I have the function GetThing as follows:

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

I want it to be able to work with different lambdas too while being able to handle other types (meaning non-lambdas, non functions, like int, and ...) , such as

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

But the problem here being the lambdas are different type, therefore the recursive function will deduced to incompatible return type, so I seems to have to use std::function like this, but it's ugly.

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

Any possible way to get around this, for example if there is an overloaded operator() then it automatically enforce the type to be std::function?

EDIT: I am aware of capture-less lambdas can be converted to a function pointer, but how to deduce it that way without std::decay in the template? Because I still want to to be handle other types as references

EDIT2: I receive a few answers utilizing std::variant, and am thinking of that, besides lambda, the parameter types shall be the same, eg. std::variant<int, int, int>. It maybe possible to add overload to GetThing, such that whenstd::variant is holding the same types, it return the thing of that type, otherwise (which is the case of receiving lambdas), returns a std::function

4 Answers

You may store your functions in an array of variants. This comes with some overhead of course. But this enables to have functions also using captured vars.

This enable to pick a function from such function collection and execute it with given parms as follows:

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



In this case GetThing also take the function parameters for calling the lambda, because the call is using std::visit. If you "only" want to pick the function, you will get the std::variant if you like and can call the function your self.


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

You can return a std::variant that contains all input types:

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

Then you need std::visit to visit your returned value:

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

For captureless lambdas you can use function pointers

#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

will work: Demo

Also note that, you need to add + for converting the lambda to function pointer. Read more: A positive lambda: '+[]{}' - What sorcery is this?


Also, in case of lambda with capture you need to use std::function. The above one will not be enough/will not work!

As solved by @康桓瑋 a variant is what you want when you have a sum type.

Here is what I think is a cleaner solution.

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

An enumeration is a variant of compile time values.

They are a runtime store of a compile time value which can then be used.

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

A variant_index_t is an enumeration of 0 to N-1. It can be used to interact with variants.

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

now we can make an enumeration for a variant from a size and a run-time element.

This is annoying machinery, but at least it is generic.

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

see, no recursion.

Test code:

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

Live example.

Output:

3.14
2
hello
goodbye
-3.14
Related