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,dropare 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)