# Extending functions to tuples

## Intro:

Imagine you have defined addition on types A and B with function A add(A,A) and B add(B,B). Now, you would like to have an addition function on tuple of these types, like tuple<A,B> add(tuple<A,B>,tuple<A,B>).

When we generalize this problem, we would like to extend an existing function working on some types to a function working on tuples of these types.

I call this extension function static_container_functor and the following code demonstrate its use:

int main() {

std::tuple<double, int> t1{3.14159, -1}, t2{2.71828, 2};

auto print = [](auto x) { std::cout << x << " "; };
auto tuple_print = static_container_functor(print);
tuple_print(t1); // prints "3.14159 -1 " and returns void instead of std::tuple<void,void> which is not possible

std::cout << std::endl;

auto add       = [](auto &&x, auto &&... y) { return (x + ... + y); };
auto t3 = tuple_add(t1, t1, t2); // t3 is of type std::tuple<double,int>
tuple_print(t3);                 // prints "9.00146 0 "

return 0;
}


I hope that it is clear to you by now what I'm trying to solve.

One way to think about static_container_functor is that it is somewhat glorified for_each for std::tuple.

## What I would like to know:

I would like to know how can I possibly improve my code and whether I'm missing handling some cases. Also, I'm have attempted to do perfect forwarding and I do not know how to test if it works.

Furthermore, I have not employed the standard trick with std::index_sequence in the main implementation of static_container_functor but did a small variation of it to keep all of the code together in one function. I would like to know what do you think about it?

How to handle constexpr? In the example, how can I achieve that the tuple t3 can be constexpr when t1 and t2 are constexpr?

The function is called static_container_functor and not tuple_functor because I would like to extend it such that is works on std::variant and std::array. Ideally on anything that implements std::get<>. Any tips in that direction would be great.

Right now I'm doing a partial check, with static_assert, that the input arguments are really std::tuples. Would you recommend using SFINAE like in this post? However, for that I would need thatstd::enable_if works with automatic return type deduction.

## Code:

https://godbolt.org/g/eWKxLo

#include <array>
#include <iostream>
#include <tuple>

template <std::size_t... I>
constexpr auto integral_sequence_impl(std::index_sequence<I...>) {
return std::make_tuple((std::integral_constant<std::size_t, I>{})...);
}

template <std::size_t N, typename Indices = std::make_index_sequence<N>>
constexpr auto integral_sequence = integral_sequence_impl(Indices{});

template <typename Op>
auto static_container_functor(Op &&op) {
return [&op](auto &&c, auto &&... d) {
/* here I should add a test that c and d... are really tuples */

constexpr int N = std::tuple_size_v<std::remove_reference_t<decltype(c)>>;
static_assert(
(true == ... ==
(N == std::tuple_size_v<std::remove_reference_t<decltype(d)>>)),
"All of the arguments must have the same length!");

auto implementation = [&](auto... I) {
auto slice = [&](auto idx) {
return std::forward_as_tuple(std::get<idx>(c), std::get<idx>(d)...);
};

auto result = [&](auto idx) {
return std::apply(op, std::forward<decltype(slice(idx))>(slice(idx)));
};
auto zero = std::integral_constant<size_t, 0>{};

if constexpr /* Are all return types are void? Return void*/
((std::is_same_v<void, decltype(result(I))> && ... && true)) {

(result(I), ...);
return;

} else /* All other cases. Return std::tuple  */ {
using ReturnType = std::tuple<decltype(result(I))...>;
return ReturnType{result(I)...};
}
};

return std::apply(implementation, integral_sequence<N>);
};
}

int main() {

std::tuple<double, int> t1{3.14159, -1}, t2{2.71828, 2};

auto print = [](auto x) { std::cout << x << " "; };

auto tuple_print = static_container_functor(print);
tuple_print(t1); // prints "3.14159 -1 " and returns void

std::cout << std::endl;

auto add       = [](auto &&x, auto &&... y) { return (x + ... + y); };
auto t3 = tuple_add(t1, t1, t2); // t3 is of type std::tuple<double,int>
tuple_print(t3);                 // prints "9.00146 0 "

return 0;
}


# Compilation warnings

   auto zero = std::integral_constant<size_t, 0>{};


What's this for? It doesn't seem to be used, and removing it doesn't affect operation.

With that fixed, I get a nice clean compilation with my usual warnings g++ -std=c++17 -g -Wall -Wextra -Wwrite-strings -Wpedantic -Warray-bounds -Weffc++.

# Includes and namespaces

<array> and <iostream> aren't required by the template, so shouldn't go with it when moved to a header. To make that clear, I'd defer including them until just before main().

integral_sequence_impl() and integral_sequence are internals, so should go into a "details" namespace that's not part of the public interface.

# Cleverness

This is probably too cute:

constexpr int N = std::tuple_size_v<std::remove_reference_t<decltype(c)>>;
static_assert(
(true == ... ==
(N == std::tuple_size_v<std::remove_reference_t<decltype(d)>>)),
"All of the arguments must have the same length!");


It looks like we believe in a Python-style chainable == operator, although that's not what's actually happening here. I think that could trip up future maintainers, who may try to "correct" it.

I'd prefer to replace it with this more-transparent equivalent using &&:

constexpr auto N = std::tuple_size_v<std::remove_reference_t<decltype(c)>>;
static_assert((... &&
(N == std::tuple_size_v<std::remove_reference_t<decltype(d)>>)),
"All of the arguments must have the same length!");


(I changed N to be auto, since the standard leaves the type of std::tuple_size unspecified.)

# Simplifications

We don't need && true at the end of the std::is_same_v fold expression.; && ... is sufficient. Actually, I think we really ought to be using || ..., for those rare cases where only some overloads return void.

We could inline slice() (but then need a comment to replace the informative name):

    auto result = [&](auto idx) {
return std::apply(op, std::forward_as_tuple(std::get<idx>(c), std::get<idx>(d)...));
//                          ^^^ slice of idx'th element from from each input ^^^
};


# Tests

The definition of add can be simplified:

auto add = []( auto&&... x) { return (x + ...); };


It would be good to see tests of functions that modify their arguments, to ensure that lvalues are passed correctly. A simple variant is to change the type of x here:

  auto add = [](auto& x, auto&&... y) { return x = (x + ... + y); };


Along with

  tuple_print(t1);                 // prints "9.00146 0 "
std::cout << '\n';


And the output does indeed match t3.

We can also create a test where the tuples to be operated on contain different types:

auto t1 = std::tuple<double, int>{3.14159, -1};
auto const t2 = std::tuple<float, unsigned char>{2.71828, 2};


I'm happy to report that this also works flawlessly (including in conjunction with the modifying test).

We could also include tests with other tuple-like types, such as std::array (I've confirmed that we can successfully mix types, using e.g. a tuple and an array of the same length - though a more realistic use-case would be a plain array and a std::array).

As indicated in the description, we're missing a test of the perfect forwarding. One way to demonstrate this works it to perform a test on a non-copyable type, such as std::unique_ptr:

auto concat = []( auto&&... x) { return (*x + ...); };
auto s1 = std::tuple<std::unique_ptr<std::string>>{std::make_unique<std::string>("foo")};
auto s2 = std::tuple<std::unique_ptr<std::string>>{std::make_unique<std::string>("bar")};
tuple_print(static_container_functor(concat)(s1,s2));
std::cout << '\n';


# Style

Minor point, but most C++ authors would write the function accepting a forwarding reference as …functor(Op&& op) rather than …functor(Op &&op). The latter, being unusual, makes me double-scan that line every time I read it (and also couple of other places that have right-binding &&).