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

std::tuple foreach implementation

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

Problem

I wrote a "foreach" implementation for std::tuple:

#pragma once

#include 
/**
 * Callback example:

struct Call{
    float k=0;

    template        // lambda function not efficient than this. Tested -O2 clang, gcc 4.8
    inline void call(T &&t){
        std::cout 
    struct LOOP{
        template 
        static inline void wind(Tuple&& tuple, Callback&& callback){
            callback.template call(tuple)), index> (std::get(tuple));
            LOOP::wind( std::forward(tuple), std::forward(callback) );
        }
    };

    template
    struct LOOP_BACK{
        template 
        static inline void wind_reverse(Tuple&& tuple, Callback&& callback){
            callback.template call(tuple)), size>( std::get(tuple) );
            LOOP_BACK::wind_reverse( std::forward(tuple), std::forward(callback) );
        }
    };

    // stop specialization
    template
    struct LOOP {
        template 
        static inline void wind(Tuple&& , Callback&& ){
            // end
        }
    };
    template
    struct LOOP_BACK{
        template 
        static inline void wind_reverse(Tuple&& , Callback&& ){
            // end
        }
    };
}

template
static void inline iterate_tuple(Tuple&& tuple, Callback&& callback){
    TUPLE_ITERATOR::LOOP::type >::value >
            ::template wind( std::forward(tuple), std::forward(callback) );
}

template
static void inline iterate_tuple_back(Tuple&& tuple, Callback&& callback){
    TUPLE_ITERATOR::LOOP_BACK::type >::value-1 >
            ::template wind_reverse( std::forward(tuple), std::forward(callback) );
}

// Call:
// iterate_tuple(Callback(), std::make_tuple(1,2,3,"asdaa"));


But I look at how other folks do that, and I see that they do this in another way. They get an array of indices, and then recursively call the callback function. Is my implementation worse than that? I ask this because if I call tuple_iterator twice, with the same parameters, the compiler starts to use asm "calls". Look HERE, on the r

Solution

Loki's solution does not enforce the order in which the function calls are performed, because the order in which function arguments are evaluated is unspecified. Here's a C++14 solution that ensures the function is called from left to right:

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


I use swallow{f(x)...} to force the evaluation order. It works because the order in which the arguments to a brace initializer are evaluated is the order in which they appear. You can then use it like:

#include 

int main() {
    for_each(std::make_tuple(1, '2', 3.3), [](auto x) {
        std::cout << x << std::endl;
    });
}


EDIT

I modified the code so it works on both GCC and Clang. Here's a more in-depth explanation of for_each_impl.

First, we make sure that we call f inside a braced initializer, so the evaluation order is from left to right:

using swallow = int[];
swallow{f(std::get(tuple))...};


But then, what if f does not return an integer value? What if it returns void for example? So we use the comma operator to make sure the expression is an integer which can be used inside the braced initializer:

swallow{(f(std::get(tuple)), int{})...};


The expression (f(stuff), int{})... is a parameter pack expansion. It expands to (f(stuff_1), int{}), (f(stuff_2), int{}), ..., (f(stuff_n), int{}), so each expression is really an int, except that some side effect has been performed before. Then, to avoid nasty overloads of the comma operator by whatever is returned by f, we insert a void between f(...) and int{}. Since operator,(SomeType, void) can't be overloaded, this ensures that the builtin operator, is used, which is what we want. This might seem overkill, but we do this in highly generic code where we must assume that f(...) could overload operator,:

swallow{(f(std::get(tuple)), void(), int{})...};
                                      ^~~~ Make sure the builtin operator, is used


Then, what happens if for_each_impl is sent 0 arguments? We're gonna try to create a 0-sized array, so we must make sure the array always has at least one element in it. We use a dummy int for this:

swallow{1, (f(std::get(tuple)), void(), int{})...};
        ^~~~ Now the array always has at least one element in it


We're almost done, but now there's an anoying compiler warning saying "You're creating a temporary array 'swallow' which is never used". To silence it, I cast the swallow{...} to void. Finally, just add perfect forwarding of the Tuple and you're done:

(void)swallow{1, (f(std::get(std::forward(tuple))), void(), int{})...};
^^^^^^ Silence warning                ^^^^^^^^^^^^^^^^^^^ Perfect forwarding


Note that the way I use std::forward here could be unsafe in other circumstances. This is because tuple could be double-moved-from if the function I forwarded it to had different characteristics. Consider:

swallow{f(function_that_moves_from_its_arg(std::forward(tuple)))...};


Now, tuple might be moved-from several times:

swallow{
    f(function_that_moves_from_its_arg(std::forward(tuple))), // move here
    f(function_that_moves_from_its_arg(std::forward(tuple))), // move here
    f(function_that_moves_from_its_arg(std::forward(tuple))), // move here
    ...
}


However, I know std::get is a friendly function and so there's should be no problem in doing this. There's an alternative way to do it "safely", but it involves using std::tuple_element and it's more complicated.

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

int main() {
    for_each(std::make_tuple(1, '2', 3.3), [](auto x) {
        std::cout << x << std::endl;
    });
}
using swallow = int[];
swallow{f(std::get<Indices>(tuple))...};
swallow{(f(std::get<Indices>(tuple)), int{})...};
swallow{(f(std::get<Indices>(tuple)), void(), int{})...};
                                      ^~~~ Make sure the builtin operator, is used

Context

StackExchange Code Review Q#51407, answer score: 27

Revisions (0)

No revisions yet.