Getting Started with C++20 Ranges: Write Cleaner and Safer Loops
If you've ever written a loop that filters, transforms, or slices a collection manually, you know how verbose and error-prone it can get.
C++20 introduces the <ranges>
library, which allows you to express these operations clearly and safely, using a declarative and composable style.
In this article, we'll walk through the essentials of C++20 ranges — focusing on real-world use cases that will make your code shorter, safer, and more readable.
✅ What Are Ranges?
At a high level, ranges combine the container and the algorithm into a single, chainable expression.
Instead of writing this:
std::vector<int> result;
for (int x : input) {
if (x % 2 == 0) {
result.push_back(x * x);
}
}
You can now write:
auto result = input
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
This is concise, expressive, and lazy-evaluated, meaning no unnecessary copies unless you actually use the results.
✅ Basic Setup
To use ranges, include:
#include <ranges>
#include <vector>
#include <iostream>
Make sure your compiler supports C++20. For GCC or Clang, you may need to pass -std=c++20
.
✅ Practical Example: Filtering Even Numbers
std::vector<int> data = {1, 2, 3, 4, 5, 6};
auto evens = data
| std::views::filter([](int x) { return x % 2 == 0; });
for (int x : evens) {
std::cout << x << " ";
}
// Output: 2 4 6
No need to write an explicit if
in the loop — just describe what you want.
✅ Transforming Values: Square Each Number
auto squares = data
| std::views::transform([](int x) { return x * x; });
for (int x : squares) {
std::cout << x << " ";
}
// Output: 1 4 9 16 25 36
Perfect for applying a function to every element.
✅ Combining Views: Filter and Then Transform
Here's where ranges shine — chaining multiple views:
auto even_squares = data
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : even_squares) {
std::cout << x << " ";
}
// Output: 4 16 36
This kind of code used to take 5+ lines and intermediate containers — now it's just one pipeline.
✅ Taking and Dropping Elements
You can slice ranges without dealing with iterators:
auto first_three = data
| std::views::take(3);
for (int x : first_three) {
std::cout << x << " ";
}
// Output: 1 2 3
Likewise:
auto skip_two = data
| std::views::drop(2);
for (int x : skip_two) {
std::cout << x << " ";
}
// Output: 3 4 5 6
No begin() + n
needed!
✅ Converting Views to Containers
Views are lazy — they don’t create new containers unless you explicitly request one:
std::vector<int> result(
even_squares.begin(), even_squares.end()
);
Alternatively, use a helper library like range-v3 or std::ranges::to
in C++23.
✅ Why Should You Use Ranges?
✅ Removes boilerplate loops
✅ Avoids intermediate containers
✅ Makes intent clearer
✅ Encourages functional composition
✅ More type-safe than raw iterators
⚠️ Gotchas
- Views don’t own data — if the original container goes out of scope, the view becomes invalid.
- Not all STL algorithms are
ranges
-enabled yet (std::ranges::sort
, etc. is available though). - May have compilation issues on older compilers or incomplete standard libraries.
Final Thoughts
C++20 ranges bring modern, declarative iteration to C++.
If you've ever felt envious of LINQ in C#, or Python list comprehensions — this is your new best friend in C++.
✍️ Summary
-
std::views::filter
,transform
,take
,drop
are your new loop tools - Ranges let you focus on what to do, not how
- Safer, more elegant, and often faster than manual loops
Have you tried ranges in your own codebase yet? Let me know how it changed your workflow — or what hurdles you're running into.
Follow me for more modern C++ tips — and feel free to share your favorite ranges
trick in the comments!
Top comments (0)