c++loopsiteratorstdnested-loops

Iterator for nested loops unrolling in C++


Very often I have nested loops at least two-level depth where elements from the same container interact which each other. Imagine interaction of particles in physics.

This leads to two cases of the code:

struct vec2f {
    float x, y;
};

void each_to_each(std::vector<vec2f>& vec)
{
    std::for_each(std::begin(vec), std::end(vec), [&]([[maybe_unused]] const auto& pos1) {
        const auto it = std::begin(vec) + (&pos1 - std::data(vec));
        std::for_each(it + 1, std::end(vec), [&]([[maybe_unused]] const auto& pos2) {
            // Some actions with pos1 and pos2 (like particles interaction)
        });
    });
}

void each_to_each_symmetrical(std::vector<vec2f>& vec)
{
    std::for_each(std::begin(vec), std::end(vec), [&]([[maybe_unused]] const auto& pos1) {
        std::for_each(std::begin(vec), std::end(vec), [&]([[maybe_unused]] const auto& pos2) {
            if (&pos1 == &pos2) {
                return;
            }

            // Some actions with pos1 and pos2 (like particles interaction)
        });
    });
}

Which I don’t like for too verbose syntax, difficulties to exit from inner loop, nested brackets, etc.

So, typical way to avoid it is to put these loops into a wrapper:

template<typename Functor>
void each_to_each_with_functor(std::vector<vec2f>& vec, Functor fn)
{
    std::for_each(std::begin(vec), std::end(vec), [&](const auto& pos1) {
        const auto it = std::begin(vec) + (&pos1 - std::data(vec));
        std::for_each(it + 1, std::end(vec), [&](const auto& pos2) {
            fn(pos1, pos2);
            });
        });
}

void foo([[maybe_unused]]const vec2f& pos1, [[maybe_unused]] const vec2f& pos2) {
    // Some actions with pos1 and pos2 (like particles interaction)
}

Which works fine, but keeps the issue with exit from the nested loop and additionally brings issue with passing the context (local variables defined before the loop) into the function; when there are many of them, this is too verbose. Sometimes, performance could be an issue as well.

My idea is to implement a kind of iterator and wrapper to it which will incapsulate processing of nested loops (with pair of iterators) and which will be able to work in single-level loop for the developer.

So the usage should be like:

    std::vector<vec2f> vec;
    NestedLoopIterator loop = NestedLoopIterator(vec.begin(),vec.end());

    std::for_each(loop.begin(), loop.end(), [&](const auto& its) {
        const vec2f& pos1 = its.it1;
        const vec2f& pos2 = its.it2;

        // Some actions with pos1 and pos2 (like particles interaction)
    });

It could define traversing strategies (like two shown at the beginning of the question), handle multithreading, could be configurable for nesting levels, etc.

Just to avoid reinventing the wheel, is there known implementation of such iterator?

If not and you see significant drawbacks of such solution and flaws in the proposed draft of design, please share your input. I am still in two minds if this is a good idea and that benefits would overweight the introduced complexity.

Here is the demo without NestedLoopIterator implementation so far.


Solution

  • If you can use c++23 (or c++20 with a little adaptation), you could use coroutines and generators.

    Like you can define generators in Python, it becomes possible in c++ to write the logic of your iterations the way you want (like you did in each_to_each for instance) and once you have the item you want to be used, you can make it available with co_yield.

    For the end user, one has just to use the function as a generator:

    #include <vector>
    #include <iostream>
    #include <generator> 
    
    template<typename T, typename U=T::value_type>
    auto each_to_each (T& vec) -> std::generator<std::pair<U,U>>
    {
        for (auto it1=std::begin(vec); it1 != std::end(vec); ++it1)
        {
            auto it = std::begin(vec) + std::distance(std::begin(vec), it1) + 1;
            
            for (auto it2=it; it2 != std::end(vec); ++it2)
            {
                co_yield std::make_pair (*it1,*it2); 
            }
        }
    }
    
    
    int main() 
    {
        std::vector<int> vec {1, 2, 3, 4};
    
        for (auto item : each_to_each(vec))
        {
            std::cout << item.first << " " << item.second << "\n"; 
        }
           
        return 0;
    }
    
    

    Demo

    This is somehow a lazy way to get for-range loops for free when writing a full fledged iterator seems tedious.


    Update: I simplified and corrected the example (ie. +1 when defining it) and mostly remove const for the input parameter (temporary objects not possible here).


    Update: Here is an interesting post about coroutines and performance.