The latest C++ Standard has been with us for a while now, and in this article we’re going to take a look at a couple of useful enhancements to the range-for syntax we’ve used since C++11. Iterating over the elements in a container is a common idiom in C++, and there are several ways of achieving this (using keyword for or the Standard Library’s std::for_each). The two patterns this article covers are iterating with a numerical index, and outputting with a separator.
Here is some code to print out a std::vector line-by-line with a numerical index:
void print_indexed(std::ostream& os, const auto& container, std::size_t start = 0) {
for (auto index = start; const auto& element : container) {
os << index++ << ": " << element << '\n';
}
}
This is fairly recent code, with an initializer in the range-for loop and an auto parameter for the container. At only three lines it’s fairly compact, the only gotcha is remembering to increment index at a suitable point within the loop. When called from client code such as:
std::vector vd{ 1.1, 2.2, 3.3, 4.4, 5.5 };
print_indexed(std::cout, vd, 1);
The output is:
1: 1.1
2: 2.2
3: 3.3
4: 4.4
5: 5.5
In C++23 it is possible to use std::views::enumerate combined with a range-based for-loop with deduction. The function print_indexed() can be rewritten as:
void print_indexed(std::ostream& os, const auto& container, std::size_t start = 0) {
for (auto&& [index, element] : container | std::views::enumerate) {
os << (index + start) << ": " << element << '\n';
}
}
A few things to note about this code, which produces the same output as before:
numbers | std::views::enumerate: This view produces a sequence of{ index, element }pairs, whereindexis the 0-based index of the element, andelementis the element from the container.- The range-based for-loop with deduction
for (auto&& [index, element] : ...)automatically destructures the pair into theindexandelementvariables, with their types deduced. - This ranges-based approach is both concise and expressive, leveraging structured bindings to easily access both the index and the value in an efficient and type-safe way.
Moving onto outputting with a separator, here is an example function to achieve this:
void print_separated(std::ostream& os, const auto& container, std::string_view separator = ", "sv) {
auto sep = ""sv;
for (const auto& elem : container) {
os << sep << elem;
sep = separator;
}
}
The initialization of variable sep at the end of the range-for loop ensures that all output except the first iteration is prefixed with the supplied separator. When called with code such as:
std::vector vd{ 1.1, 2.2, 3.3, 4.4, 5.5 };
std::cout << "[ ";
print_separated(std::cout, vd);
std::cout << " ]\n";
The output produced is:
[ 1.1, 2.2, 3.3, 4.4, 5.5 ]
With the availability of the Ranges library (since C++20, with additions in C++23), it is possible to rewrite the function print_separated():
void print_separated(std::ostream& os, const auto& container, const std::string& separator = ", "s) {
for (auto&& element : container
| std::views::transform([](const auto& value) { return std::to_string(value); })
| std::views::join_with(separator)) {
os << element;
}
}
A number of things to note about this function:
- An additional stage in the pipeline uses a lambda with
std::views::transformto convert each element incontainerto astd::string. - The
std::views::join_with()stage of the pipeline appends a separator to all elements except the last one. - Again, the type deduction with
for (auto&& element : ...)is utilized.
The output produced is the same as before.
In conclusion, we’ve seen how range-based for loops have been extended with auto&& type deduction for efficient use of references to both container and view elements, for both single element types and with destructuring for pairs. This allows for easier to maintain code and more flexibility when dealing with per-element access for C++ container types.