Q: Большая вложенность циклов Предположим, что имеется некоторая большая структура  
struct Level3First {
    int field;
};

struct Level3Second {
    int field;
};

struct Level2 {
    std::vector<Level3First> firstContainer;
    std::vector<Level3Second> secondContainer;
};

struct Level1 {
    std::vector<Level2> firstContainer;
};

struct Level0 {
    std::vector<Level1> firstContainer;
    std::vector<Level1> secondContainer;
};

Не ищите в ней смысла, она искусственная. В реальном коде она больше, осмысленнее и уровни имеют большое количество других полей.
Далее, в коде часто встречаются различные обходы этой структуры, например  
void foo0( const Level0 &lvl0 ) {
    for ( const auto &lvl1 : lvl0.secondContainer ) {
        for ( const auto &lvl2 : lvl1.firstContainer ) {
            for ( const auto &lvl3 : lvl2.firstContainer ) {
                // do something with lvl3.field
            }
        }
    }
}

void foo1( const Level1 &lvl1 ) {
    for ( const auto &lvl2 : lvl1.firstContainer ) {
        for ( const auto &lvl3 : lvl2.secondContainer ) {
            // do something with lvl3.field
        }
    }
}

void foo2( const Level0 &lvl0 ) {
    for ( const auto &lvl1 : lvl0.secondContainer ) {
        for ( const auto &lvl2 : lvl1.firstContainer ) {
            // do something with lvl2
        }
    }
}

Можно ли как-то избавиться от этой кучи циклов?
Пока в голову не приходит ничего кроме написания шаблонных-функций, вроде такой  
template<class T, class Fun>
    void foo3( const T &container, Fun fun ) {
        for ( const auto &member : container ) {
            fun( member );
        }
    }

void foo4( const Level0 &lvl0 ) {
    foo3( lvl0.firstContainer, []( const auto &vals) {
            // do something with vals
        } );
}

, однако, если потребуется сделать что-либо с конкретным полем на втором уровне (Level2), мне придётся либо писать функцию 
template<class T, class Fun>
    void foo5( const T &containerFromLvl0, Fun fun ) {
        for ( const auto &lvl1 : lvl0.firstContainer ) {
            for ( const auto &lvl2 : lvl1.firstContainer ) {    //а также аналогичная 
                    //шаблонная функция, для обхода lvl1.secondContainer
                fun( member );
            }
        }
    }

, либо писать нечто подобное
void fun6( const Level0 &lvl0 ) {
    foo3( lvl0.firstContainer, []( const auto &vals ) {
            foo3( vals.firstContainer, []( const auto &vals ) {
                    foo3( vals.firstContainer, []( const auto &vals ) { //далее может 
                                //появиться аналогичная функция, с обходом 
                                //vals.secondContainer
                            // do something with vals
                        } );                    
                } );
        } );
}

, что выглядит в разы хуже большой вложенности циклов, а как потом такое сопровождать я даже думать не хочу.
В результате, пока наиболее адекватным мне кажется написание серии шаблонных функций вроде foo3 и foo5, под каждый из возможных (как минимум часто встречающихся) обходов, но мне кажется, что должен быть подход проще.
Какие ещё есть способы избавиться от этой вложенности, либо как можно привести вышеописанные примеры к лучшему виду?

A: Как мне кажется, это классическая задача для Visitor паттерна: пишется класс/шаблон обхода интересующих структур, в метод обхода которого передаётся функция, знающая что делать с последним уровнем.
template <class Container, class LeafFunction> class Visitor {
    static void visit(Container& c, LeafFunction f) {}
}
template <class LeafFunction> class Visitor<Level0> {
    visit(Level0& c, LeafFunction f) { 
       Visitor<Level1, LeafFunction>::visit(c.firstContainer, f)
       Visitor<Level1, LeafFunction>::visit(c.secondContainer, f)
    }
}
template <class LeafFunction> class Visitor<Level1> {
    ... аналогично
}
template <class LeafFunction> class Visitor<Level3> {
    visit(Level3& c, LeafFunction f) {
        f(c.field);
    }
}

Соответственно, f может быть много чем, и каждый вариант обхода тоже может быть представлен аналогичным шаблоном.

A: как вариант можно попробовать заинкапсулировать ваши данные 
struct c_lvl_1
{
public:
   void foo() { /* realization here */ };

protected:
   std::vector<c_lvl_2> data;
}

struct Root 
{
public:
   void foo(int container number);

protected:
    c_lvl_1 container1;
    c_lvl_1 container2;
};

void Root::foo(int container_number)
{
    switch(container_number)
    {
       case 1: return container1->foo();
       case 2: return container2->foo();
       ...
    }

    // throw exception no_container
}

Т.о. вы прячете данные и вместо них предоставляете интерфейс методов для работы с ними (поиск, модификация и т.п.). На каждом уровне иерархии за реализацию методов будет ответственен соответствующий класс

A: Я бы попробовал реализовать рекурсивный подход, если это не сильно бьет по производительности
template <typename Tuple, typename Func, size_t... IArgs>
void foreach_impl_call(Tuple& tuple, Func func, std::index_sequence<IArgs...> is){
    func(std::get<IArgs>(tuple)...);
}

template <size_t Index, typename Tuple, typename Func, typename Is>
void foreach_impl(Tuple& tuple, Func func, Is is){
    foreach_impl_call(tuple, func, is);
}

template <size_t Index, typename Tuple, typename Func, typename Is, typename Container, typename... Args>
void foreach_impl(Tuple& tuple, Func func, Is is, Container& container, Args&... args){
    for(const auto& c: container){
        std::get<Index>(tuple) = c;
        foreach_impl<Index + 1, Tuple, Func, Is, Args...>(tuple, func, is, args...);
    }
}

template <size_t... IArgs, typename Func, typename Container, typename... Args>
void foreach(Func func, Container& container, Args&... args){
    using Tuple = std::tuple<typename Container::value_type, typename std::remove_reference_t<Args>::value_type...>;
    using Is = std::index_sequence<IArgs...>;
    Tuple tuple;
    foreach_impl<0, Tuple, Func, Is, Container, Args...>(tuple, func, Is(), container, args...);
}

Вызывать можно так:
vector<int> c1 = {...};
vector<int> c2 = {...};
foreach<0, 1>([](int x, int y){ std::cout << x << " " << y << std::endl; }, c1, c2);

