c++algorithmloopstemplatesvariadic-templates

Variadic function to loop through Cartesian product of multiple spans


Span referred to a continuous memory, for example an array;

template <typename T>
class Span {
public:
    Span(T first, size_t s) ...;
    Span(T first, T lasst) ...;
public:
    // iterating underlying continuous memory;
    begin() ...
    end() ...
    T & operator[](idx) ...
private:
    T first;
    T last;
};

loop function can iterating all passing spans, like a nested bubbling loop;

template <typename Func, typename ...Spans>
void loop(Func func, Spans &&...spans) {
    // question: how to implements ?
}

example

int main() {
    int arr[] {1, 2, 3};
    float arr2[] {1.1, 2.2};
    int arr3[] {-1, -2, -3};
    
    loop(func, Span(arr, 3), Span(arr2, 2));
    /*
        we can get:
        func(1, 1.1);
        func(1, 2.2);
        func(2, 1.1);
        func(2, 2.2);
        func(3, 1.1);
        func(3, 2.2);
    */

    loop(func, Span(arr, 3), Span(arr2, 2), Span(arr3, 3));
    /*
        we can get:
        func(1, 1.1, -1);
        func(1, 1.1, -2);
        func(1, 1.1, -3);
        func(1, 2.2, -1);
        func(1, 2.2, -2);
        func(1, 2.2, -3);
        func(2, 1.1, -1);
        func(2, 1.1, -2);
        ...
    */
}

how to implements the loop function, using c++ language features up to c++20 is ok, but no standard library or 3rd-part library, just implements manually;

no recursion function call, better for loop; cause recursion easily get stack overflow;

using vector and tuple, structure binding is ok;


Solution

  • Here is an alternative that uses an array to keep track of the indices for each of the spans. The index array is declared to have nSpans+1 elements where the first element serves as a sentinel. The array spanSizes is declared to be the same size and also have a sentinel. The variable carryIndex keeps track of which index should be incremented. The word "carry" here refers to the fact that the indices are incremented in a manner similar to carries in additions.

    template<class Fn, class... Spans>
    void loop(Fn&& fn, Spans... spans) {
        constexpr std::size_t nSpans = sizeof...(spans);
        // Array containing the sizes of each span, with a sentinel in front
        std::size_t spanSizes[]{0, spans.size()...};
        // Array containing the loop indexes, also with a sentinel in front
        std::size_t indexes[nSpans+1]{};
        // Points to the index that should be incremented, after considering carries from less significant indexes
        std::size_t carryIndex = nSpans;
        // While the sentinel (most significant index) is not reached
        while(indexes[0] == 0){
            // Call `fn` with perfect forwarding, passing in elements picked from each span according to the corresponding index.
            // The +1 accounts for the sentinel.
            [&]<std::size_t... Is>(std::index_sequence<Is...>){
                std::forward<Fn>(fn)(spans[indexes[Is+1]]...);
            }(std::make_index_sequence<nSpans>());
            // Increment the current index.
            ++indexes[nSpans];
            // Loop while there's need to carry to a more significant index
            while(indexes[carryIndex] == spanSizes[carryIndex]){
                // Reset the index.
                indexes[carryIndex] = 0;
                // Update `carryIndex` to point to the next more significant index and increment it.
                ++indexes[--carryIndex];
    
                // The next increment should start over from the least significant index.
                if(indexes[carryIndex] != spanSizes[carryIndex]){
                    carryIndex = nSpans;
                }
            }
        }
    }
    

    Demo: https://godbolt.org/z/ejzhe7e5G (The main() is taken from demo in the answer by @Turtlefight)