Last Update:
Combining Collections with Zip in C++23 for Efficient Data Processing
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);
}
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 salesproducts
- what types of products were soldsales
- 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);
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, checkviews::keys
,views::values
or more genericviews::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 useviews::abc
rather thanranges::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.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: