Compile-time recursion in C++17

While not a large upgrade to the C++ standard, C++17 brought some important features. One of them helps me have simpler code in the context of compile-time recursion.

I’ve experimented with some strategies to iterate over a tuple. I wanted a list of different types that I could iterate over as I would do with an array. Without using dynamically allocated memory. Some kind of static polymorphism.

The previous article on this topic gives an implementation for C++14. Which is not that complicated, but whenever I can get something simpler, I’m very interested. That article is the base for this one, thus reading it before will give more context.

I want to improve the iteration that is based on compile-time recursion. More specifically, the exit case of the recursion. The C++14 implementation is based on template specialization. There are two versions of the iterator struct: one iterates over the elements of the tuple except for the last one, and the second one is for the last element, where the iteration must stop.

template<typename T, std::size_t S = std::tuple_size<T>::value, std::size_t I = S - 1>
struct Iterator {
    template<typename C>
    void operator()(T& objects, C callback) {
        callback(std::get<S - I - 1>(objects));
        Iterator<T, S, I - 1>{}(objects, callback);
    }
};

template<typename T, std::size_t S>
struct Iterator<T, S, 0> {
    template<typename C>
    void operator()(T& objects, C callback) {
        callback(std::get<S - 1>(objects));
    }
};

The code is partially duplicated and not the easiest to understand. Any piece of code that can be deleted is the best code I can get. And constexpr if lets me do just that. I can delete one of the structs and have the implementation, including the exit case, in one struct. The constexpr if feature allows the use of an if statement in more complex compile-time cases.

template<typename T, std::size_t I = 0U>
struct Iterator {
    template<typename C>
    void operator()(T& objects, C callback) {
        if constexpr (I < std::tuple_size_v<T>) {
            callback(std::get<I>(objects));
            Iterator<T, I + 1U>{}(objects, callback);
        }           
    }
};

I have an easier-to-read code. The iteration starts naturally from zero, and as long as I’m not past the last element, I apply the given callback, then get to the next element.

Simplicity is always welcome.