Table of Contents

If you have a standard container, it’s easy to use a range-based for loop and iterate over its elements at runtime. How about std::tuple? In this case, we cannot use a regular loop as it doesn’t “understand” tuple’s compile-time list of arguments. That’s why in this article, I’ll show you a few techniques you can use to run through all tuple’s entries.

As a use case, let’s consider the following code:

std::tuple tp { 10, 20, 3.14, 42, "hello"};
printTuple(tp);

We’d like to have the following output on the console:

(10, 20, 3.14, 42, hello)

How to implement such a printTuple function?

Let’s start!

This is the first part where we discuss the basics. See the second part here where we talk about generalizations, std::apply, and more.

The basics  

std::tuple is a a fixed-size collection of heterogeneous values.

For comparison, its smaller friend - std::pair - takes two template parameters, <T, U>.

std::pair<int, double> intDouble { 10, 42.42 };
// or with CTAD, C++17:
std::pair deducedIntDouble { 10, 42.42 }; // deduced!

std::tuple takes a variable number of arguments. So it’s a generalization of std::pair because it can take any number of arguments/values.

std::tuple<int, double, const char*> tup {42, 10.5, "hello"};
// or with CTAD, C++17:
std::tuple deducedTup {42, 10.5, "hello"}; // deduced

If you want to access a pair’s element you can just ask for .first or .second entry:

std::pair intDouble { 10, 42.42 }; 
intDouble.first *= 10;

On the other hand, since tuple has a variable size there’s no .first or .third… you can only access it through std::get:

#include <tuple>
#include <iostream>

int main() {
    std::tuple tp {42, 10.5, "hello"};
  
    // by index:
    std::get<0>(tp) *= 100;
    std::cout << std::get<0>(tp) << '\n';
    std::cout << std::get<2>(tp) << '\n';
    
    // by type:
    std::cout << std::get<double>(tp) << '\n';
}

See at @Compiler Explorer.

How to iterate?  

Ok, we know some basics, and now we can try to build some code that would run through all elements of such a tuple.

As you can see, the values/types are set at compile time. This is different than a regular container like std::vector, where we usually push values at runtime.

To iterate through tuple, we’d like to transform this “imaginary” code:

// imaginary:
std::tuple tp {42, 10.5, "hello"};
for (auto& elem : tp)
    std::cout << elem << ", ";

Into something like:

std::tuple tp {42, 10.5, "hello"};
std::cout << std::get<0>(tp) << ", ";
std::cout << std::get<1>(tp) << ", ";
std::cout << std::get<2>(tp) << ", ";

In other words, we need to expand out tuple into a set of std::get<id> invocations to access an element at position id. Later we can pass this obtained element into std::cout or any other callable object (to process it).

Unfortunately, the language doesn’t support such compile-time loops… yet (see at the bottom for more information).

To achieve a similar effect, we need to apply some template techniques.

Preparations  

At first, we can try with the following function template that takes the list of indices we’d like to print:

template <typename T>
void printElem(const T& x) {
    std::cout << x << ',';
};

template <typename TupleT, std::size_t... Is>
void printTupleManual(const TupleT& tp) {
    (printElem(std::get<Is>(tp)), ...);
}

And we can try the following demo:

std::tuple tp { 10, 20, "hello"};
printTupleManual<decltype(tp), 0, 1, 2>(tp);

See @Compiler Explorer.

What happens here?

Our printTupleManual takes not only a tuple type, but also non-type template arguments - a variadic list of size_t values.

In that call, I used decltype to deduce the type of tp. Alternatively we could call it like:

std::tuple tp { 10, 20, "hello"};
printTupleManual<std::tuple<int, int, const char*>, 0, 1, 2>(tp);

As you can see, decltype save a lot of typing in this case. See more at Cppreference - decltype.

Inside the function, we use a fold expression (available since C++17) to expand this variadic parameter pack over a comma operator.

In other words our function will instatiate to the following form:

void printTupleManual<std::tuple<int, int, const char *>, 0, 1, 2>
    (const std::tuple<int, int, const char *> & tp)
{
  printElem(get<0>(tp)), (printElem(get<1>(tp)), printElem(get<2>(tp)));
}

We can see this “expansion” thanks to CppInsights - see a demo at this link.

Thanks to fold expressions, we’re very close to our “compile-time loop”!

Meet index_sequence  

In the previous example, we had to pass a list of indices manually. Creating such an argument list is not scalable and error-prone. Can we automatically deduce such a list based on the size of the tuple?

All we want is to generate the following indices:

// for a tuple of size N generate
0, 1, ..., N-1

This problem is quite common in template programming, and since C++14, we can use index_sequence. Which is a helper class template that holds indices in the form of non-type template parameters:

template< class T, T... Ints > 
class integer_sequence;

template<std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;
The C++ Standard Library defines std::integer_sequence<T, T... Ints>, but then std::index_sequence is just integer_sequence over size_t. See @cppreference.com.

We can transform our code into:

template <typename T>
void printElem(const T& x) {
    std::cout << x << ',';
};

template <typename TupleT, std::size_t... Is>
void printTupleManual(const TupleT& tp, std::index_sequence<Is...>) {
    (printElem(std::get<Is>(tp)), ...);
}

And call it as follows:

std::tuple tp { 10, 20, "hello"};
printTupleManual(tp, std::index_sequence<0, 1, 2>{});

We can also use a helper function make_index_sequence:

printTupleManual(tp, std::make_index_sequence<3>{});

And the final part: get the size of the tuple:

printTupleManual(tp, std::make_index_sequence<std::tuple_size<decltype(tp)>::value>{});

There’s also a helper variable template: tuple_size_v, so it can make our line a bit shorter:

printTupleManual(tp, std::make_index_sequence<std::tuple_size_v<decltype(tp)>>{});

We can now wrap everything into the following function:

template <typename T>
void printElem(const T& x) {
    std::cout << x << ',';
};

template <typename TupleT, std::size_t... Is>
void printTupleManual(const TupleT& tp, std::index_sequence<Is...>) {
    (printElem(std::get<Is>(tp)), ...);
}

template <typename TupleT, std::size_t TupSize = std::tuple_size_v<TupleT>>
void printTupleGetSize(const TupleT& tp) {
    printTupleManual(tp, std::make_index_sequence<TupSize>{});
}

And now we can call it in a super simple way:

std::tuple tp { 10, 20, "hello"};
printTupleGetSize(tp);

See code @Compiler Explorer.

We can also see the full “Expanded” code through C++ Insights: this link.

For example our call expands into:

void printTupleGetSize<std::tuple<int, int, const char *>, 3>
     (const std::tuple<int, int, const char *> & tp)
{
  printTupleManual(tp, std::integer_sequence<unsigned long, 0, 1, 2>{});
}

As you can see make_index_sequence was nicely expanded into std::integer_sequence<unsigned long, 0, 1, 2>{}.

Printing std::tuple  

We have all the core functionality for iteration, so we can try wrapping it in the final print function.

template <typename TupleT, std::size_t... Is>
void printTupleImp(const TupleT& tp, std::index_sequence<Is...>) {
    size_t index = 0;
    auto printElem = [&index](const auto& x) {
        if (index++ > 0) 
            std::cout << ", ";
        std::cout << x;
    };

    std::cout << "(";
    (printElem(std::get<Is>(tp)), ...);
    std::cout << ")";
}

template <typename TupleT, std::size_t TupSize = std::tuple_size_v<TupleT>>
void printTuple(const TupleT& tp) {
    printTupleImp(tp, std::make_index_sequence<TupSize>{});
}

As you can see, I also converted the printElem function into a lambda inside the printTupleImp function. This is a helper step as it allows me to easily carry some additional state to the printing function. I need to check if I should put a separator or not.

Now we can run it:

std::tuple tp { 10, 20, "hello"};
printTuple(tp);

Have a look @Compiler Explorer.

It’s nice!

But… how about making it more operator << friendly? Right now, the function is tightly coupled with the std::cout stream object, so it’s hard to print tuple into some other output.

Operator <<  

All we need is to use our helper function and pass additional ostream object:

#include <iostream>
#include <ostream>
#include <tuple>

template <typename TupleT, std::size_t... Is>
std::ostream& printTupleImp(std::ostream& os, const TupleT& tp, std::index_sequence<Is...>) {
    size_t index = 0;
    auto printElem = [&index, &os](const auto& x) {
        if (index++ > 0) 
            os << ", ";
        os << x;
    };

    os << "(";
    (printElem(std::get<Is>(tp)), ...);
    os << ")";
    return os;
}

template <typename TupleT, std::size_t TupSize = std::tuple_size<TupleT>::value>
std::ostream& operator <<(std::ostream& os, const TupleT& tp) {
    return printTupleImp(os, tp, std::make_index_sequence<TupSize>{}); 
}

int main() {
    std::tuple tp { 10, 20, "hello"};
    std::cout << tp << '\n';
}

See here @Compiler Explorer.

Adding indices  

Since we have an index list, why not use it?

#include <iostream>
#include <ostream>
#include <tuple>

template <typename TupleT, std::size_t... Is>
std::ostream& printTupleImp(std::ostream& os, const TupleT& tp, std::index_sequence<Is...>) {
    auto printElem = [&os](const auto& x, size_t id) {
        if (id > 0) 
            os << ", ";
        os << id << ": " << x;
    };

    os << "(";
    (printElem(std::get<Is>(tp), Is), ...);
    os << ")";
    return os;
}

template <typename TupleT, std::size_t TupSize = std::tuple_size<TupleT>::value>
std::ostream& operator <<(std::ostream& os, const TupleT& tp) {
    return printTupleImp(os, tp, std::make_index_sequence<TupSize>{}); 
}

int main() {
    std::tuple tp { 10, 20, "hello"};
    std::cout << tp << '\n';
}

See @Compiler Explorer

Now, instead of having a separate index variable, I just pass the current index of the element from the fold expression. We can also use it and print it as the prefix for each element.

And we get:

(0: 10, 1: 20, 2: hello)

Would you like to see more?
If you want to see a similar code that works with C++20's std::format, you can see my article: How to format pairs and tuples with std::format (~1450 words) which is available for C++ Stories Premium/Patreon members. See all Premium benefits here.

Summary & Next Part  

That was a cool experiment!

Through several steps of this tutorial, we went from the basics of tuples into iteration with manual index list and then std::index_sequence. Thanks to fold expressions available in C++17, we can expand our compile-time argument list and apply a function over it.

We focused on the printing function, as it’s relatively easy to understand and fun. Next time, I’ll try to make our iteration function more general to allow also some transformations on the values. We’ll also see a handy function std::apply that adds other options.

See the second part: C++ Templates: How to Iterate through std::tuple: std::apply and More - C++ Stories.

Back to you

I also wonder about your use cases for tuples and iteration over it.

Let us know your thoughts in the comments below the article.

Compile time for...

I mentioned that C++ doesn’t offer a compile-time loop, but there’s a proposal P1306 - “Expansion statements”. It’s currently in revision but unlikely to appear in C++23.

It would allow something like:

auto tup = std::make_tuple(0, a, 3.14);
for... (auto elem : tup)
    std::cout << elem << std::endl;

See the status of this paper @Github/cpp/papers.

Notes  

Books:

Articles and links: