In this article, we’ll look at a super handy ranges view in C++23 - views::zip. In short, it allows you to combine two or more ranges and iterate through them simultaneously. Let’s see how to use it.

Basic  

If you have two (or more) separate containers and you want to iterate through them “in parallel,” you can write the following code:

std::vector a { 10, 20, 30, 40, 50 };
std::vector<std::string> b { "ten", "twenty", "thirty", "fourty" };

for (size_t i = 0; i < std::min(a.size(), b.size()); ++i)
    std::cout << std::format("{} -> {}\n", a[i], b[i]);

We can even use iota to generate indices:

// range version
for (auto i : std::views::iota(0uz, std::min(a.size(), b.size())))
    std::cout << std::format("{} -> {}\n", a[i], b[i]);
As of C++23 you can use the integer literal suffix for std::size_t is any combination of z or Z with u or U (i.e. zu, zU, Zu, ZU, uz, uZ, Uz, or UZ).

Or extract some names rather than using indices:

for (size_t i = 0; i < std::min(a.size(), b.size()); ++i) {
    const auto& num = a[i];
    const auto& name = b[i];
    std::cout << std::format("{} -> {}\n", num, name);
}

Play with all examples @Compiler Explorer

As you can see, we can easily iterate through those containers, but the code is not elegant. What’s more, you have to pay attention to details and make sure you use the min size from all collections.

In C++23, we have a nicer solution: zips!

#include <format>
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector a { 10, 20, 30, 40, 50 };
    std::vector<std::string> b { "one", "two", "three", "four" };
        
    for (const auto& [num, name] : std::views::zip(a, b))
        std::cout << std::format("{} -> {}\n", num, name);
}

See @Compiler Explorer

How cool is that? And you can already play with this in GCC 13 (as well as partially in Clang 15, and MSVC 17.3).

Suppose you iterate over two ranges zip yields std::pair and std::tuple if you have more ranges. My code used structured binding to unpack those tuples into meaningful names.

This iteration might be especially handy when your data is split across many containers - like in SOA (Structure of Arrays) rather than AOS (Array of structs).

Note: for having a container with just indices, you can have a look at views::enumerate (also C++23)

But how about a more practical example?

Walking over three containers  

Let’s say you have three containers:

  • dates - dates of the sales
  • products - what types of products were sold
  • sales - income from each types
std::vector<std::string> dates = {
    "2023-03-01", "2023-03-01", "2023-03-02", "2023-03-03", "2023-03-03"
};
std::vector<std::string> products = {
    "Shoes", "T-shirts", "Pants", "Shoes", "T-shirts"
};
std::vector sales = {
    50.0, 20.0, 30.0, 75.0, 40.0
};

We want to group those data and check what income we got on a given date, or how much we got selling a given category of products.

That’s to zip we can write the following code:

std::map<std::string, double> salesInDay;
std::map<std::string, double> salesPerProduct;
for (const auto & [d, p, s] : std::views::zip(dates, products, sales)) {
    salesInDay[d] += s;
    salesPerProduct[p] += s;
}

for (const auto& [day, sale] : salesInDay)
    std::cout << std::format("in {} sold {}\n", day, sale);

for (const auto& [prod, sale] : salesPerProduct)
    std::cout << std::format("sold {} in {} category\n", sale, prod);

Run @Compiler Explorer

The Output:

in 2023-03-01 sold 70
in 2023-03-02 sold 30
in 2023-03-03 sold 115
sold 30 in Pants category
sold 125 in Shoes category
sold 60 in T-shirts category

Zip transform  

But there’s more. What if we’d like to compute the income based on prices and costs:

int main() {
    std::vector prices = {100, 200, 150, 180, 130};
    std::vector costs = {10, 20, 50, 40, 100};     
    
    std::vector<int> income;    
    income.reserve(prices.size());
    for (const auto& [p, c] : std::views::zip(prices, costs))
        income.emplace_back(p - c); // <<
    
    std::cout << std::accumulate(income.begin(), income.end(), 0);
}

The code above uses zip, and then stores the computation in the income vector. But thanks to the related view: zip_transform, we can write:

int main() {
    std::vector prices = {100, 200, 150, 180, 130};
    std::vector costs = {10, 20, 50, 40, 100};     
    
    std::vector<int> income;    
    income.reserve(prices.size());
    auto compute = [](const auto& p, const auto& c) { return p - c; };
    auto v = std::views::zip_transform(compute, prices, costs);
    for (const auto& in : v)
        income.emplace_back(in);
    
    std::cout << std::accumulate(income.begin(), income.end(), 0);
}

This time, we use a separate view that walks on elements of both containers but then applies the compute callable object.

Play @Compiler Explorer

Waiting for ranges::to and creating containers  

Right now, you can use range-based for loops to create other containers, but in hopefully a couple of weeks, you’ll be able to write:

std::vector prices = {100, 200, 150, 180, 130};
std::vector costs = {10, 20, 50, 40, 100};    

auto compute = [](const auto& p, const auto& c) { return p - c; };
auto v = std::views::zip_transform(compute, prices, costs);
auto income = v | std::ranges::to<std::vector>();

Briefly, ranges::to allows you to pack various views and subranges into a separate container That you can process later.

To unzip a map, check views::keys, views::values or more generic views::elements. See at cppreference.com.

Summary  

In this short article, we looked at a handy view called std::views::zip. It allows you to “walk” on elements of several containers at once.

Before C++20/23, you might be familiar with boost combine or ranges v3 that allowed for such iteration technique. But for example, boost combine wasn’t prepared for structured binding.

If you wonder why it’s better to use views::abc rather than ranges::abc_view see this related article by Barry R.: Prefer views::meow | Barry’s C++ Blog

Back to you

Do you use zip views in your code? Maybe you used some custom code or the boost approach?

Join the discussion below, or at reddit/cpp.