Search code examples
c++tuples

making a std::tuple from the result of a pack


Suppose I'm writing a function called 'invoke_all', which applies all the lambdas it receives (as template parameters) to the arguments, it looks like:

template <auto... fs, typename... Args>
constexpr auto invoke_all(Args &&...args) {
  (std::invoke(fs, std::forward<Args>(args)...), ...);
}

so far so good. call it like:

invoke_all<
      [](auto i, auto j) { std::println("lambda1: {} {}", i, j); },
      [](auto i, auto j) { std::println("lambda2: {} {}", i, j); }
      >
      (42, "abc"s);

but suppose I actually want those lambdas to return values, and pack all the return values in a std::tuple, the problem arises. This doesn't compile:

#include <functional>
#include <tuple>
#include <format>
#include <string>
#include <cassert>

template <auto... fs, typename... Args>
constexpr auto invoke_all(Args &&...args) {
  return std::tuple{(std::invoke(fs, std::forward<Args>(args)...), ...)};
}

using namespace std::literals;

int main() {
    auto results = invoke_all< //
        [](auto i, auto j) { return std::format("lambda1: {} {}", i, j); },
        [](auto i, auto j) { return std::format("lambda2: {} {}", i, j); }> //
    (42, "abc"s);

    assert((results == std::tuple{"lambda1: 42 abc"s, "lambda2: 42 abc"s}));
}

because the tuple that 'invoke_all' returns, is a tuple of size 1, not 2.

and this doesn't compile either:

#include <functional>
#include <tuple>
#include <format>
#include <string>
#include <cassert>

template <auto... fs, typename... Args>
constexpr auto invoke_all(Args &&...args) {
  return std::tuple{std::invoke(fs, std::forward<Args>(args)...), ...};
}

using namespace std::literals;

int main() {
    auto results = invoke_all< //
        [](auto i, auto j) { return std::format("lambda1: {} {}", i, j); },
        [](auto i, auto j) { return std::format("lambda2: {} {}", i, j); }> //
    (42, "abc"s);

    assert((results == std::tuple{"lambda1: 42 abc"s, "lambda2: 42 abc"s}));
}

because the missing () around the pack expansion (I think).

So is there a way out of this, i.e., making a tuple from the result pack expansion?


Solution

  • You almost had it with your last attempt. The thing you need to remember is that ... means expand into a comma separated list so when you do

    template <auto... fs, typename... Args>
    constexpr auto invoke_all(Args &&...args) {
      return std::tuple{std::invoke(fs, std::forward<Args>(args)...), ...};
    }
    

    You are trying to do a comma expression but it is missing the outer () required to do a fold expression. Instead what you want is

    template <auto... fs, typename... Args>
    constexpr auto invoke_all(Args &&...args) {
      return std::tuple{std::invoke(fs, std::forward<Args>(args)...)...};
    }
    

    which doesn't have a ,. This expands out to

    template <auto... fs, typename... Args>
    constexpr auto invoke_all(Args &&...args) {
      return std::tuple{std::invoke(fs1, fwd_arg1, fwd_arg2, ..., fwd_argN), 
                        std::invoke(fs2, fwd_arg1, fwd_arg2, ..., fwd_argN)
                        ...,
                        std::invoke(fsN, fwd_arg1, fwd_arg2, ..., fwd_argN);
    }
    

    One additional thing to note with your test case, the result will not be a

    std::tuple{"lambda1: 42 abc"s, "lambda2: 42 abc"s}
    

    but is instead

    std::tuple{"lambda1: 42 abc"s, "lambda2: 42"s}
    

    This is because "abc"s is a temporary so it gets moved into the first lambda and then you are left with an empty string for the rest of the lambdas. Ideally you should not forward anything and just pass by value so everything is treated as an lvalue like

    template <auto... fs, typename... Args>
    constexpr auto invoke_all(Args &&...args) {
      return std::tuple{std::invoke(fs, args...)...};
    }