9

I have two vectors. I want to iterate over all elements of both and do something (say print them out). So I could write something like:

vector<int> vec_a{1, 2, 3}, vec_b{4, 5, 6, 7};

for (auto a : vec_a) {
  cout << a;
}
for (auto b : vec_b) {
  cout << b;
}

This has a lot of duplication. I could do something like:

for (const auto& vec : {vec_a, vec_b}) {
  for (auto elem : vec) {
    cout << elem;
  }
}

But this adds an extra for (which is not too bad but I'm wondering if there is something better. Something along the lines of:

for (auto elem : concat(vec_a, vec_b)) {
  cout << elem;
}

I know I could just concat the vectors (a la Concatenating two std::vectors) but that syntax is even clunkier (especially since I actually have 4 vectors).

I want the output to be:

1 2 3 4 5 6 7

Benjy Kessler
  • 7,356
  • 6
  • 41
  • 69
  • Have a look at this post: https://stackoverflow.com/questions/8511035/sequence-zip-function-for-c11 – ctenar Oct 15 '20 at 13:02
  • it is unclear whether you want to concatenate the vectors as in `{1,2}, {3,4}` -> `{1,2,3,4}` and then process each element individually or if you want to iterate them in parallel and process pairs `{1,3}` and `{2,4}`, the second snippet looks like the latter, the first like the first – 463035818_is_not_an_ai Oct 15 '20 at 13:02
  • I guess within the standard library (before C++20) you can't really do anything. Take a look at `boost::range::join`. – Evg Oct 15 '20 at 13:03
  • @idclev, I think all examples iterate over the elements squentially. There is no requirement that they have the same number of elements and indeed they don't in my case. – Benjy Kessler Oct 15 '20 at 13:04
  • maybe add example input and output, then it would be clear without doubts – 463035818_is_not_an_ai Oct 15 '20 at 13:04
  • @ctenar, that question is about zipping. I don't want to zip the vectors just to iterate over them one after the other. – Benjy Kessler Oct 15 '20 at 13:05
  • 1
    @idclev, added. – Benjy Kessler Oct 15 '20 at 13:06
  • What do you feel bad in @Jarod42's [answer](https://stackoverflow.com/a/45563644/3545273) to the linked question (`for (auto e : ranges::view::concat(v1, v2)) { ...`)? – Serge Ballesta Oct 15 '20 at 13:06
  • 2
    @Serge Ballesta I think the biggest problem with that is that it was 20 answers down and I didn't see it :) – Benjy Kessler Oct 15 '20 at 13:09

3 Answers3

11

A simple solution is to use a helper function:

#include <functional>

template <typename Func, typename... Containers>
void for_all(Func&& func, Containers&&... containers) {
    auto iteration_func = [&](auto&& container) {
        for (auto&& elem : std::forward<decltype(container)>(container)) {
            std::invoke(func, std::forward<decltype(elem)>(elem));
        }
    };

    (iteration_func(std::forward<Containers>(containers)), ...);
}

Here, we use a fold expression with an immediately invoked lambda to simulate a loop over the variadic template arguments, where each of them is looped over and the provided function object is invoked on its elements.

The use of forwarding references and invocations to std::forward preserve the value categories of arguments and elements, for compatibility with rvalue ranges (e.g., move_view from the range-v3 library). std::invoke generalizes the notion of function objects to pointers to members, which can be useful in certain cases.

Example:

int main() {
    std::vector<int> vec_a{1, 2, 3};
    std::vector<int> vec_b{4, 5, 6, 7};
    
    for_all([](int n) {
        std::cout << n << ' ';
    }, vec_a, vec_b);
    
    std::cout << '\n';
}

(wandbox)

Different container types can be mixed:

for_all([](const auto& n) {
    std::cout << n << ' ';
}, std::vector{1, 2, 3}, std::list{"foo", "bar"});
L. F.
  • 19,445
  • 8
  • 48
  • 82
  • 3
    I like your definition of simple :) – Benjy Kessler Oct 15 '20 at 13:13
  • 1
    @BenjyKessler as in simple usage :) The templates are to make it as general as possible without losing the conciseness. – L. F. Oct 15 '20 at 13:14
  • Does this require all the vectors to be of the same type or is it enough that they all support ostream operator? I guess not since you pass "int n" to the for_all call. Do you know how to adapt this solution to a vec_b being strings and not ints? – Benjy Kessler Oct 15 '20 at 13:37
  • @BenjyKessler Just change `int` to `const auto&` in the lambda. Each vector can have a different type. In fact, all containers supporting the ranged `for` loop can be used, not just `vector`. – L. F. Oct 15 '20 at 13:41
  • I assume wrapping the lambda in parentheses and doing (lambda(), ...) is calling the lambda on each of the variadic template args but what is that syntax? I've never seen that before. Do you have a reference for that? – Benjy Kessler Oct 15 '20 at 18:39
  • @BenjyKessler Your understanding is correct. `(pattern, ...)` is a [fold expression](https://en.cppreference.com/w/cpp/language/fold) that expands the pattern to a sequence of operands separated by the comma operator. Using an immediately invoked lambda with it is the best thing we can currently do. – L. F. Oct 16 '20 at 10:26
  • Thanks, I hope you don't mind I edited your response to document the templates. – Benjy Kessler Oct 18 '20 at 06:43
  • @BenjyKessler That's definitely fine. I've added some information about the other concepts used here and moved the explanation out of the code for legibility. – L. F. Oct 18 '20 at 06:55
1

There is nice range library which works with C++14 and in large part will become part of C++20. In this library there is ranges::views::concat which does exactly what you need in nice clean way:

#include <iostream>
#include <ranges>
#include <vector>
#include <array>
#include <functional>
#include <range/v3/view/concat.hpp>

int main()
{
    using ranges::views::concat;
    
    auto a = std::vector<int>{ 1, 2, 6, 7 };
    auto b = std::array<int , 2>{ 1, 3 };
    auto c = { -1, -4, 7, 8, 9, 11};

    for (auto x : concat(a, b)) {
        std::cout << x << ", ";
    }
    std::cout << "\n--\n";

    for (auto x : concat(c, a, b)) {
        std::cout << x << ", ";
    }

    return 0;
}

https://godbolt.org/z/Waf3Ma

Sadly concat is not available in C++20 (can't find it in documentation and gcc do not support it).

Marek R
  • 32,568
  • 6
  • 55
  • 140
0

If you want to reduce the code, you can simply write a function to iterate:

void iterate(const std::vector<int>& t)
{
    for(auto i : t)
    {
        std::cout << i << ' ';
    }
}

vector<int> a{1, 2, 3}, b{4, 5, 6};

iterate(a);
iterate(b);

With that function written, you can take an adaptation of L.F.'s idea above and write a function that will iterate over any number of arrays you pass:

template <class ... T>
void iterate_many(T ... args)
{
    (void) std::initializer_list<int>{
        ((void) iterate(args), 0)...
    };
}

Here's the full example:

#include <iostream>
#include <vector>
using namespace std;
 
void iterate(const std::vector<int>& t)
{
    for(auto i : t)
    {
        std::cout << i << ' ';
    }
}
 
template <class ... T>
void iterate_many(T ... args)
{
    (void) std::initializer_list<int>{
        ((void) iterate(args), 0)...
    };
}
 
int main() {
    vector<int> a{1, 2, 3}, b{4, 5, 6}, c{7, 8, 9};
 
    iterate(a);
    iterate(b);
 
    std::cout << '\n';
 
    iterate_many(a, b, c);
 
    return 0;
}

ideone

Chad
  • 18,706
  • 4
  • 46
  • 63