12
\$\begingroup\$

I have implemented a static_for loop to iterate over the elements of an std::tuple using C++17 features. Here is what I have currently:

#include <iostream>
#include <utility>
#include <tuple>

template <std::size_t I>
struct wrapper
{
    static constexpr std::size_t n = I;
};

template <class Func, std::size_t ...Is>
constexpr void static_for_impl( Func &&f, std::index_sequence<Is...> )
{
    ( f( wrapper<Is>{} ),... );
}

template <std::size_t N, class Func>
constexpr void static_for( Func &&f )
{
    static_for_impl( f, std::make_index_sequence<N>{ } );
}

int main()
{
    auto t = std::make_tuple( 1, 22, 3, 4 );

    std::size_t weighted = 0;
    static_for<4>( [&](auto w)
    {
        weighted += ( w.n + 1 ) * std::get<w.n>( t );
    });

    std::cout << "Weighted: " << weighted << std::endl;

    return 0;
}

Demo

My questions are:

  1. Is there a more standard way that this can be implemented (like maybe using std::apply or invoke)?

  2. Is there another idiom that would be better than the ugly wrapper struct that can be used to access the index at compile time from within a lambda?

Accepted modification: Incorporated all the suggested modifications here.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ The trouble with this design you have bad coupling. If you change the size of your tuple you need to find all instances of your static_for() and modify them with the new size. You should be able to pass the tuple as a parameter and get it automatically called for each value. \$\endgroup\$ Aug 21 '17 at 16:52
  • 2
    \$\begingroup\$ PS. A tuple which only contains one type is really called an array. \$\endgroup\$ Aug 21 '17 at 16:53
  • \$\begingroup\$ Please see What to do when someone answers. I have rolled back Rev 2 → 1. \$\endgroup\$ Aug 21 '17 at 17:17
8
\$\begingroup\$

IMHO, I think this is indeed pretty ugly. Assuming you just want to iterate over all of the elements of tuple (which is what you've done here), you can simplify this both in implementation and in usage:

template <class Tup, class Func, std::size_t ...Is>
constexpr void static_for_impl(Tup&& t, Func &&f, std::index_sequence<Is...> )
{
    ( f(std::integral_constant<std::size_t, Is>{}, std::get<Is>(t)),... );
}

template <class ... T, class Func >
constexpr void static_for(std::tuple<T...>&t, Func &&f)
{
    static_for_impl(t, std::forward<Func>(f), std::make_index_sequence<sizeof...(T)>{});
}

int main()
{
    auto t = std::make_tuple( 1, 22, 3, 4 );

    std::size_t weighted = 0;
    static_for(t, [&] (auto i, auto w) { weighted += (i+1) * w; });

    std::cout << "Weighted: " << weighted << std::endl;

    return 0;
}

This is more like for_each, or maybe for_each + enumerate from python. On the implementation side, you no longer need this wrapper stuff. On the usage side, the lambda just directly gets passed the index and the the element, so it doesn't need to extract it from the tuple. Note that I pass the integer by "type" so that you can do crazy things with it but for most purposes it would probably just be simpler to pass an actual size_t as the first argument. Though integral_constant does implicitly convert anyhow.

If you wanted to iterate over multiple tuples or something like that (very rare), you could take my approach and modify it so that the lambda only gets passed the first argument, and then captures the tuple and uses std::get<i> to index into it. But you still don't need the wrapper struct or w.n.

\$\endgroup\$
2
  • \$\begingroup\$ Will this work for different types? Aren't you fixing a single signature for f when you call static_for, as opposed to expanding the parameter pack with the generic lambda? \$\endgroup\$
    – einpoklum
    Aug 24 '17 at 16:53
  • \$\begingroup\$ @einpoklum A generic lambda still has a fixed type. It's like a class with a templated operator(). \$\endgroup\$ Aug 24 '17 at 17:30
2
\$\begingroup\$

Nir covered the fundamental concept of the algorithm. Nevertheless, there are 2 important points that were missed.

Hardcoding cv qualifiers and lvalue referenceness

One could use std::tuple_size_v<std::remove_reference<Tuple>> to determine count of elements, then follow up with std::invoke() to forward the functor and each element by using std::get<I>(forward(tuple)). This will propagate constness and rvalue referenceness, which should be usable due to deduction guides of C++17. Roughly it looks like this:

template <typename Tuple, typename Functor, std::size_t ElementCount = std::tuple_size<std::remove_reference_t<Tuple>>::value>
constexpr void static_for(Tuple&& t, Functor&& f)
{
    //Use ElementCount to construct the indices list and pass to impl
}

All of this leads me to the next point:

std::tuple is not the only tuple

std::array is considered tuple too. There might be more tuples to come in the future. Using std::tuple_size_v will make everything uniform.

\$\endgroup\$
8
  • \$\begingroup\$ You don't need any of these techniques for array, because all of the elements have the same type so you can just use a normal for loop. As for hardcoding cv qualifiers, I wouldn't say I missed it. There are many ways to do it with different pros/cons. Writing overloads for several different cv qualifiers is pretty much the standard way, e.g. used by std::get itself. Your point about tuple_size_v is reasonable, though I'm not sure how much practical concern this is. \$\endgroup\$ Aug 21 '17 at 19:03
  • \$\begingroup\$ @Incomputable std::apply passes all the elements to the functor at once, not one at a time like is desired here. \$\endgroup\$
    – aschepler
    Aug 22 '17 at 0:24
  • \$\begingroup\$ @aschepler, you're totally right. Fixed. \$\endgroup\$ Aug 22 '17 at 0:30
  • \$\begingroup\$ "Use std::tuple_size_v - use it where? \$\endgroup\$
    – einpoklum
    Aug 24 '17 at 16:54
  • 1
    \$\begingroup\$ @einpoklum, use it to detect if passed in type is tuple, if it is it will give its size. If not it will give compilation error. \$\endgroup\$ Aug 24 '17 at 17:03
1
\$\begingroup\$

I improved the code based on the response by Nir Friedman. Although I missed to mention it in my original post, he also correctly identified the need to iterate over multiple tuples.

Loki Astari also correctly commented on the need to auto deduce the proper size and I incorporated some modifications to accommodate for this. I added the functionality to iterate using more or less regular for-loop semantics. The nice part is that bound checking is performed by the compiler so this is pretty safe.

Here is my revised code that can be used to operate on std::tuple, std::array and similar types:

#include <iostream>
#include <utility>
#include <tuple>
#include <experimental/array>

using std::cout;

template <typename T, T Begin,  class Func, T ...Is>
constexpr void static_for_impl( Func &&f, std::integer_sequence<T, Is...> )
{
    ( f( std::integral_constant<T, Begin + Is>{ } ),... );
}

template <typename T, T Begin, T End, class Func >
constexpr void static_for( Func &&f )
{
    static_for_impl<T, Begin>( std::forward<Func>(f), std::make_integer_sequence<T, End - Begin>{ } );
}

template <class Tuple>
constexpr std::size_t tuple_size( const Tuple & )
{
    return std::tuple_size<Tuple>::value;
}

int main()
{
    auto a = std::experimental::make_array( 1, 22, 3, 4 );
    auto t = std::make_tuple( 6,  5, 3, 8 );
    // auto t = std::make_tuple( 6,  5, 3 ); // Compiler will properly identify range errors and this will not compile. A static_assert would give a more meaningful error.

    std::size_t weighted = 0;

    static_for<std::size_t, 0, tuple_size( a )>( [&]( auto i)
    {
        weighted += (i+1) * std::get<i>( a )*std::get<i>( t );
    });

    cout << "Weighted: " << weighted << std::endl;

    cout << "Loop over integers:\n";
    static_for<int, -3, 3 >( [&]( auto i)
    {
        cout << i << ", "; 
    });
    cout << '\n';

    return 0;
}

Demo

The compiler fully optimizes out the weighted computation:

Assembly output

I feel the need to say that this type of C++17 features immensely simplify my codebase template implementation by getting rid of all kinds of template recursion tricks.

\$\endgroup\$
3
  • \$\begingroup\$ fyi either zero or almost zero of this requires 17. I have very similar things I use in a 14 codebase. \$\endgroup\$ Aug 21 '17 at 18:16
  • \$\begingroup\$ Will this work for different types? \$\endgroup\$
    – einpoklum
    Aug 24 '17 at 16:55
  • \$\begingroup\$ As long as std::get and std::tuple_size are implemented, yes. Simple specializations can extend it to almost any conceptually compliant type. \$\endgroup\$ Aug 24 '17 at 17:56

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.