Thoughts from a C++ library developer.

Implementing a tuple_iterator

This post is part of a collaboration with Arne Mertz.
Arne is a software Engineer at Zühlke and a clean code enthusiast with a focus on modern C++. You can find him online at Twitter and at his “Simplify C++!” blog.
We’ve both written something about accessing std::tuple,
but swapped our blogs - my post is over at his blog and his one follows here now:

Did you ever wonder how we could iterate over the contents of a std::tuple at runtime, similar to an array or std::vector?
You may or may not see the need for such a functionality - this walkthrough shows a proof of concept and how you tackle problems like this in C++17.

The mission

When I say “iterate over the contents of a tuple”, I think of range-based for loops. Tuples have neither begin() and end() methods, nor are we allowed to overload free versions of those functions in namespace std. That means, range-based for directly over a tuple is not possible, so we’ll have to provide a wrapper for the functionality around std::tuple.

Another problem is the content we iterate over: This should work for any instantiation of std::tuple, i.e. with arbitrary contents. The elements we iterate over will have to be some kind of sum type. The type for that in the STL is std::variant, and with std::visit we can access whatever is in it.

Here, overload is just a functionality that draws all the arguments together into one single function object.

Taking it apart

Compile time access at runtime?

Iterating over a tuple at compile time is easy. With std::get<N> we can access any member. The N, however, needs to be known at compile time. If iterators in a range-based for loop were allowed to change their type in every step, we could just write a tuple_iterator<N> template and call it a day.

But it’s not that easy. Iteration happens at runtime, and we have no arbitrary runtime access for tuples. That means, we somehow have to map runtime information (i.e. which element should the iterator point to) to the access functions that need compile time information.

The only way to achieve this is to put all of the compile-time information in a list we can iterate over at runtime. In other words, we need a lookup table.

Let’s go over this step by step: Since std::get<N> returns different types, we can not simply take the addresses of std::get<0>, std::get<1> etc. for a given tuple. We need to convert the result into a result_type common to all those functions, e.g. the std::variant I mentioned earlier.

To get that, we need a converter_fun function or function object that, applied to any element of our tuple, results in the result_type. The static function template access_tuple<N> does exactly this. Last but not least we have to stuff pointers to all those functions into our lookup table.

Filling out the blanks

We don’t want to put too much logic into this one template, so we can just use template parameters for tuple_type, return_type and converter_fun. In addition, to generate the contents of our table, we’ll need to generate indices from 0 through table_size -1 as shown here. This is a typical use case for variadic non-type templates.

Leverage type deduction

We’d like to have most of the template parameters deduced, especially since the converter function will probably be a lambda. The index parameter pack will be provided via a std::index_sequence. So let’s write a small utility function to do the type deduction for us:

Now, the only thing that has to be provided explicitly is the return type. Note that neither R nor F, nor Idxs... are specified at this point. That means, we could use this to execute any given F on our tuple, as long as it can be applied to all elements in that index list and the return types are convertible to R.

The return type

It’s time to get more concrete on that return type. I wrote it should be a std::variant. To be able to have write access to the tuple, and to not have to make potentially costly copies of the tuple elements, the variant should contain references. Sadly, std::variant may not contain references, so we’ll have to use std::reference_wrapper.

The standard library makes an effort to provide most of the functionalities that are available for std::tuple also for std::pair and std::array. Therefore, we should specialize this metafunction for those two as well. Note that for std::array this is quite useless in most cases, since it already has begin() and end() member functions.

The runtime access function

With the lookup table and the utility function, we should be able to write a function that simply takes the Nth entry of it and invokes it on a tuple to get the std::variant containing the corresponding element. All that is missing is to write the function object that does the wrapping into the std::reference_wrapper for us, and create the right std::index_sequence:

The rest is easy…

Having tackled the runtime access to the ith element of any tuple, the rest of the way to our range based for loop is relatively straight forward.

tuple_iterator

The absolute minimum for the range-based for loop is that the iterator type returned from begin() have the pre-increment and dereferencing operators defined, and that operator!= is defined for the two types returned by begin() and end(). Note that from C++17 the two types need not necessarily be the same.

For our purposes it will be sufficient if we use the same type of iterator for begin() and end(). Personally, I think operator!= should always be implemented in terms of operator==, if possible, so I’ll provide that one as well.

This code will now compile as it is and delivers the expected results. It will also “just work” for std::pair, because we took care of common_tuple_access for pairs.

Dealing with reference_wrapper

Since we had to make the tradeoff of using std::reference_wrapper inside the variant, we have to be aware of that fact. For example, if we have a generic lambda in our visitor, it will always be called with the reference_wrappers instead of the functions we intended to do the job.

In addition, if the reference wrapper contains a template like std::string, then printing it via operator<< will fail, because it will not consider the implicit conversion from std::reference_wrapper<std::string>> to std::string. Therefore, the following code will result in a template error novel:

Conclusion

We can get runtime access to tuples, although there is some overhead involved.
The redirection via the function pointer table can not be optimized away, and neither can the resolution of the variant’s content in std::visit.
We trade some performance for flexibility, because we do not need to know which element we are accessing at compile time.

Would you like to see a way to implement an operator[] that can make the clumsy std::get<N> calls on tuples much nicer without any run time overhead?
Head over to my blog for Jonathan’s solution!

This post was made possible by my Patreon supporters.
If you'd like to support me as well, please head over to my Patreon and do so!
One dollar per month can make all the difference.