19
\$\begingroup\$

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<char, 5> arr = {{'h', 'e', 'l', 'l', 'o'}};
for_each(arr, [](char c) { std::cout << c; });  // prints: hello
std::cout << '\n';

Implementation:

#include <tuple>
#include <type_traits>
#include <utility>

namespace detail {

// workaround for default non-type template arguments
template<std::size_t I>
using index_t = std::integral_constant<std::size_t, I>;

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

// specialization for empty tuple-likes 
template<typename FromIndex, typename Tuple, typename UnaryFunction>
struct for_each_t<FromIndex, index_t<0>, Tuple, UnaryFunction> {
    constexpr UnaryFunction&& operator()(Tuple&&, UnaryFunction&& f) const
    {
        return std::forward<UnaryFunction>(f);
    }
};

// specialization for last element
template<typename ToIndex, typename Tuple, typename UnaryFunction>
struct for_each_t<index_t<ToIndex::value - 1>, ToIndex, Tuple, UnaryFunction> {
    constexpr UnaryFunction&& operator()(Tuple&& t, UnaryFunction&& f) const
    {
        std::forward<UnaryFunction>(f)(
                std::get<ToIndex::value - 1>(std::forward<Tuple>(t)));
        return std::forward<UnaryFunction>(f);
    }
};

}  // namespace detail

template<typename Tuple, typename UnaryFunction>
constexpr UnaryFunction for_each(Tuple&& t, UnaryFunction&& f)
{
    return detail::for_each_t<detail::index_t<0>,
                              detail::index_t<std::tuple_size<
                                  std::remove_reference_t<Tuple>
                              >::value>,
                              Tuple,
                              UnaryFunction>()(
            std::forward<Tuple>(t), std::forward<UnaryFunction>(f));
}

Specific concerns I have:

  1. I'm passing in & out functors by universal reference rather than by value (as std::for_each does). I'm concerned whether this can cause problems. Following the advice from answerers of this post, I've changed the return type of for_each to UnaryFunction (but note the return types of for_each_t can remain unchanged).

  2. Whether the design can be simplified.

  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.)

\$\endgroup\$
  • \$\begingroup\$ Your code use template recursion which is how people originally approached this type of problem. The issue here is that compilers only have to implement so many levels of template recursion to be compliant so with bigger tuples this may start to fail on some compilers. The more modern technique is to use an iterative approach (using std::index_sequence). I don't have time this evening to look hard but if nobody has answered by tomorrow I'll dig in. \$\endgroup\$ – Martin York Mar 22 '17 at 0:21
  • \$\begingroup\$ @LokiAstari No one has answered yet. \$\endgroup\$ – Zeta Apr 13 '17 at 5:22
4
\$\begingroup\$

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

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

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

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<int, 1024>, which would simply break with your version (you would probably want std::for_each in this case, though).

\$\endgroup\$
  • \$\begingroup\$ Thanks! I assume the fold expression in C++17 would make things a lot easier? \$\endgroup\$ – Zizheng Tai May 21 '17 at 22:57
  • \$\begingroup\$ @ZizhengTai I would say that for "foreach", fold expressions don't really make it easier (maybe you can fold over the comma operator, but that doesn't seem any easier than this) - fold expressions seem more targeted at "reduce". \$\endgroup\$ – Justin May 22 '17 at 2:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.