2
votes

Let's say I want to create a lambda that executes some other lambdas in order like this:

constexpr auto some_func{[]() {
  // do something
}};

constexpr auto some_other_func{[]() {
  // do something else
}};

constexpr auto funcs_tuple = std::tuple(some_func, some_other_func);
constexpr auto combined_funcs = do_funcs(funcs_tuple);

combined_funcs();

I have implemented the do_funcs function as:

template <std::size_t Idx = 0, typename Tuple>
constexpr auto do_funcs(const Tuple& tup) {
  return [&]() {
    if constexpr (Idx < std::tuple_size_v<Tuple>) {
      const auto f = std::get<Idx>(tup);
      f();
      do_funcs<Idx + 1>(tup)();
    }
  };
}

which just executes the functions in the tuple in order. However the resulting variable combined_funcs can't be declared constexpr because the reference to funcs_tuple in the call to do_funcs is not a constant expression.

I am trying the code in Compiler Explorer with clang(trunk) and get

error: constexpr variable 'combined_funcs' must be initialized by a constant expression

note: reference to 'funcs_tuple' is not a constant expression

Why is this not considered a constant expression? Is there a way that I can make it constexpr?

After some trial and error I have found that instead of capturing the tuple by reference in the returned lambda from do_funcs but rather capturing by value the resulting lambda can indeed be declared constexpr, but I really do not want to create a copy of the tuple for each recursive call to do_funcs.

constexpr auto do_funcs(const Tuple& tup) {
  // capture by value instead of reference
  //      |
  //      v
  return [=]() { ...

I would also like to make a helper function that takes a parameter pack of lambdas and dispatches it as a tuple to the do_funcs function like this:

template <typename... Funcs>
constexpr auto make_do_funcs(Funcs&&... fs) {
  const auto funcs = std::tuple(std::forward<Funcs>(fs)...);
  return do_funcs(funcs);
}

The reason I am using tuples instead of another method like this:

template <typename Func, typename... Funcs>
constexpr auto do_funcs(Func&& f, Funcs&&... fs) {
  return [f = std::forward<Func>(f), ... fs = std::forward<Funcs>(fs)] {
    f();
    if constexpr (sizeof...(fs) > 0) {
      do_funcs(fs...);
    }
  };
}

is because "perfect capture" is a C++20 feature and requires a workaround with tuples for C++17.

For further reference I am trying to make a utility "parser" for my combination parser library which executes some other parsers to create a more complex parser when needed.

3
I'm not sure what you mean. Why can't combined_funcs be declared constexpr? Do you get an error? Also, don't you mean combined_funcs(); instead of do_funcs();?cigien
@cigien yes, I get "error: constexpr variable 'combined_funcs' must be initialized by a constant expression, note: reference to 'funcs_tuple' is not a constant expression" using clang(trunk) on Compiler ExplorerRikus Honey
Ok. Add that error to the question. The link is helpful as well.cigien

3 Answers

2
votes

What about a simply std::apply() (that folds over comma operator) as follows ?

#include <iostream>
#include <tuple>

int main()
 {
   auto some_func_1{[]() { std::cout << "1" << std::endl; }};
   auto some_func_2{[]() { std::cout << "2" << std::endl; }};

   auto funcs_tuple = std::tuple{some_func_1, some_func_2};

   auto do_funcs
      = [](auto const & tpl)
         { std::apply([](auto ... fn){ (fn(), ...); }, tpl); };

   do_funcs(funcs_tuple);
 }

You can see that you get printed 1 and 2

Removing std::couts (no constexpr compatible) and adding some constexpr, you have

#include <iostream>
#include <tuple>

int main()
 {
   constexpr auto some_func_1{[]() { }};
   constexpr auto some_func_2{[]() { }};

   constexpr auto funcs_tuple = std::tuple{some_func_1, some_func_2};

   constexpr auto do_funcs
      = [](auto const & tpl)
       { std::apply([](auto ... fn){ (fn(), ...); }, tpl); };

   do_funcs(funcs_tuple);
 }
1
votes

Make funcs_tuple static. Because it is constexpr, this shouldn't change really change anything about how your code functions, since all calls to whatever function this is should always arrive at the same value of funcs_tuple. (The differences would be if you were taking the address of it for some reason, I guess.) However, it does make references to funcs_tuple constexpr, because now there is one object represented by the constexpr variable and not one per invocation of the function. Godbolt

Note that this doesn't work for a constexpr function. Thankfully, if the enclosing function is constexpr, the variables don't need to be. That is, you can do both/either

void func() { // func not constexpr
    static constexpr auto funcs_tuple = ...;
    constexpr auto combined_funcs = do_funcs(funcs_tuple);
}

or

// like in the question
template <typename... Funcs>
constexpr auto make_do_funcs(Funcs&&... fs) {
    // funcs not constexpr or static; make_do_funcs still constexpr
    const auto funcs = std::tuple(std::forward<Funcs>(fs)...);
    return do_funcs(funcs)(); // or whatever
    // note: you CANNOT return do_funcs(funcs), because that would be a dangling reference
    // your original make_do_funcs is simply broken
}
0
votes

I have resulted to using my own helper struct to act as a lambda.

template <typename... Funcs>
struct do_funcs {
  constexpr do_funcs(Funcs... fs) : funcs{fs...} {}

  void operator()() const {
    do_rest();
  }

  template <std::size_t Idx = 0>
  void do_rest() const {
    if constexpr (Idx < sizeof...(Funcs)) {
      const auto f = std::get<Idx>(funcs);
      f();
      do_rest<Idx + 1>();
    }
  }

  const std::tuple<Funcs...> funcs;
};

which allows the example given in the question to be constexpr.