HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

for_each for tuple-likes

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
tupleforfor_eachlikes

Problem

I've implemented a C++14 for_each for tuple-like objects. It's similar to std::for_each in that it also returns the functor once it's done. Usage examples:

With a visitor functor:

// visitor functor
struct print {
    void operator()(int x) const { std::cout << "int: " << x << '\n'; }
    void operator()(double x) const { std::cout << "double: " << x << '\n'; }
};

auto t = std::make_tuple(1, 2, 3.14);
for_each(t, print());  // prints: int: 1
                       //         int: 2
                       //         double: 3.14


With a C++14 generic lambda:

auto t = std::make_tuple(1, 2, 3.14);
for_each(t, [](auto x) { std::cout << x << '\n'; });  // prints: 1
                                                      //         2
                                                      //         3.14


With a stateful functor:

struct summer {
    void operator()(int x) noexcept { sum += x; }

    int sum = 0;
};

auto t = std::make_tuple(1, 2, 3, 4, 5);
int sum = for_each(t, summer()).sum;  // sum == 15


With a std::array:

std::array arr = {{'h', 'e', 'l', 'l', 'o'}};
for_each(arr, [](char c) { std::cout << c; });  // prints: hello
std::cout << '\n';


Implementation:

``
#include
#include
#include

namespace detail {

// workaround for default non-type template arguments
template
using index_t = std::integral_constant;

// process the
From::value`-th element
template
struct for_each_t {
constexpr UnaryFunction&& operator()(Tuple&& t, UnaryFunction&& f) const
{
std::forward(f)(
std::get(std::forward(t)));
return for_each_t,
ToIndex,
Tuple,
UnaryFunction>()(
std::forward(t), std::forward(f));
}
};

// specialization for empty tuple-likes
template
struct for_each_t, Tuple, UnaryFunction> {
constexpr UnaryFunction&& operator()(Tuple&&, UnaryFunction&& f) const
{
return std::fo

Solution

I'm going to start with your concern #3:


I've seen people do it with std::index_sequence. The accepted answer
in this
post
is the shortest version I've seen, but it feels a bit like a hack.
Also, it generates longer assembly
code than my
version does. (My version generates the
exact same assembly code as a completely manually expanded
version.)

The placement of a single constexpr in the index-sequence version makes the generated assembly identical for both versions. The missing constexpr was on the for_each_impl that was doing most of the work in the index-sequence version

Concern #2:


Whether the design can be simplified.

At this point, there isn't more that I can say other than Louis Dionne's answer is much simpler than your solution:

#include 
#include 
#include 

template 
void for_each_impl(Tuple&& tuple, F&& f, std::index_sequence) {
    using swallow = int[];
    (void)swallow{1,
        (f(std::get(std::forward(tuple))), void(), int{})...
    };
}

template 
void for_each(Tuple&& tuple, F&& f) {
    constexpr std::size_t N = std::tuple_size>::value;
    for_each_impl(std::forward(tuple), std::forward(f),
                  std::make_index_sequence{});
}


Fixed by adding the two missing constexprs (one each function):

#include 
#include 
#include 

template 
constexpr void for_each_impl(Tuple&& tuple, F&& f, std::index_sequence) {
    using swallow = int[];
    (void)swallow{1,
        (f(std::get(std::forward(tuple))), void(), int{})...
    };
}

template 
constexpr void for_each(Tuple&& tuple, F&& f) {
    constexpr std::size_t N = std::tuple_size>::value;
    for_each_impl(std::forward(tuple), std::forward(f),
                  std::make_index_sequence{});
}


Compared to your code, this is much easier to understand, largely because there is much less code, as well as the fact that this style of doing something for each element of a parameter pack is pretty standard nowadays.

I know you are concerned that this feels like a hack, but I assure you, it is not really. It's the easiest way to regain a parameter pack from a tuple-like type.

Additionally, doing work with parameter packs tends to be more efficient than recursive functions (in terms of compile-time). That is not something to disregard casually.

Furthermore, compilers have a pretty small limit for constexpr recursion compared to runtime recursion. On my version of gcc, it was 500. Yes, that's not a trivially small amount and it would work for most tuples you pass to your for_each, but different compilers could choose different allowed recursion depths. Also, it's not inconceivable that I would want to have a std::array, which would simply break with your version (you would probably want std::for_each in this case, though).

Code Snippets

#include <cstddef>
#include <tuple>
#include <utility>

template <typename Tuple, typename F, std::size_t ...Indices>
void for_each_impl(Tuple&& tuple, F&& f, std::index_sequence<Indices...>) {
    using swallow = int[];
    (void)swallow{1,
        (f(std::get<Indices>(std::forward<Tuple>(tuple))), void(), int{})...
    };
}

template <typename Tuple, typename F>
void for_each(Tuple&& tuple, F&& f) {
    constexpr std::size_t N = std::tuple_size<std::remove_reference_t<Tuple>>::value;
    for_each_impl(std::forward<Tuple>(tuple), std::forward<F>(f),
                  std::make_index_sequence<N>{});
}
#include <cstddef>
#include <tuple>
#include <utility>

template <typename Tuple, typename F, std::size_t ...Indices>
constexpr void for_each_impl(Tuple&& tuple, F&& f, std::index_sequence<Indices...>) {
    using swallow = int[];
    (void)swallow{1,
        (f(std::get<Indices>(std::forward<Tuple>(tuple))), void(), int{})...
    };
}

template <typename Tuple, typename F>
constexpr void for_each(Tuple&& tuple, F&& f) {
    constexpr std::size_t N = std::tuple_size<std::remove_reference_t<Tuple>>::value;
    for_each_impl(std::forward<Tuple>(tuple), std::forward<F>(f),
                  std::make_index_sequence<N>{});
}

Context

StackExchange Code Review Q#134814, answer score: 4

Revisions (0)

No revisions yet.