Last Update:
C++20 Ranges Algorithms - 7 Non-modifying Operations
Table of Contents
C++20’s Ranges offer alternatives for most of <algorithm>'s'
. This time I’d like to show you ten non-modifying operations. We’ll compare them with the “old” standard version and see their benefits and limitations.
Let’s go.
Before we start
Key observations for std::ranges
algorithms:
- Ranges algorithms are defined in the
<algorithm>
header, while the ranges infrastructure and core types are defined in the<ranges>
header. - Usually, there are at least two overloads for range algorithms: with a pair of iterators and an overload with a single range argument.
- The version that returns a subrange or an iterator and takes a range returns a borrowed range or a borrowed iterator. This helps detect iterators to temporary ranges.
- The range versions take “projections,” which sometimes allows more flexibility; for example, you can sort against some selected members or perform additional transformations before the comparison.
- See my separate article on this powerful feature: C++20 Ranges, Projections, std::invoke and if constexpr - C++ Stories
- The ranges version doesn’t have a parallel execution option (you cannot pass the
std::execution
policy). - The ranges algorithms, similarly to standard algorithms as of C++20, are also
constexpr
. - As of C++20, there are no ranges numerical algorithms corresponding to the
<numeric>
header.
Below, you can find examples showing a standard algorithm and an alternative version with ranges. They illustrate some basic concepts and try not to use advanced ranges composition or views. We’ll go with the order found at cppreference/algorithms, and in this part, we’ll cover “Non-modifying sequence operations.”
This is the first part of the on Ranges Algorithms. See
- the second article on “11 Modifying Operations”.
- the third article on “sorting, searching, other, and C++23 parts”.
1. all_of
, any_of
, none_of
A standard algorithm:
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
int main() {
const std::vector nums = {1, 2, 3, -4, 5, 6, 7, 8 };
auto is_positive = [](const auto& v) { return v > 0; };
// standard version:
auto res = std::all_of(begin(nums), end(nums), is_positive);
std::cout << "std::all_of: " << res << '\n';
res = std::any_of(begin(nums), end(nums), is_positive);
std::cout << "std::any_of: " << res << '\n';
}
And the ranges version:
// ranges version:
res = std::ranges::all_of(nums, is_positive);
std::cout << "std::ranges::all_of: " << res << '\n';
res = std::ranges::any_of(nums, is_positive);
std::cout << "std::ranges::any_of: " << res << '\n';
Play @Compiler Explorer
We can also write a more complex example where scan a container of custom types:
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
struct Product {
std::string name_;
double value_ { 0.0 };
};
int main() {
const std::vector<Product> prods {
{ "box", 10.0 }, {"tv", 100.0}, {"none", -1.0}
};
auto is_positive = [](const auto& v) { return v > 0; };
auto is_positive_val = [](const Product& p) {
return p.value_ > 0;
};
// standard version:
auto res = std::all_of(begin(prods), end(prods), is_positive_val);
std::cout << "std::all_of: " << res << '\n';
res = std::any_of(begin(prods), end(prods), is_positive_val);
std::cout << "std::any_of: " << res << '\n';
// ranges version:
res = std::ranges::all_of(prods, is_positive, &Product::value_);
std::cout << "std::ranges::all_of: " << res << '\n';
res = std::ranges::any_of(prods, is_positive, &Product::value_);
std::cout << "std::ranges::any_of: " << res << '\n';
}
Play @Compiler Explorer
In the ranges version, we can still use is_positive
, a generic predicate, but I used a projection that only “takes” Product::value_
and passes it into the predicate. In the standard case, I had to write a custom lambda aware of the Product
type.
2. for_each
An alternative to a good range based for loop:
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
struct Product {
std::string name_;
double value_ { 0.0 };
};
int main() {
const std::vector<Product> prods {
{ "box", 10.0 }, {"tv", 100.0}, {"none", -1.0}
};
auto out = [](const auto& v) { std::cout << v << ", "; };
// standard version:
std::cout << "std::for_each: \n";
std::for_each(begin(prods), end(prods), [](const Product& p){
std::cout << p.name_ << ", " << p.value_ << '\n';
});
std::cout << "std::for_each only names reverse: \n";
std::for_each(rbegin(prods), rend(prods), [](const Product& p){
std::cout << p.name_ << '\n';
});
// ranges version:
std::cout << "std::ranges::for_each: \n";
std::ranges::for_each(prods, [](const Product& p) {
std::cout << p.name_ << ", " << p.value_ << '\n';
});
std::cout << "std::ranges::for_each only names in reverse: \n";
std::ranges::for_each(prods | std::views::reverse,
out, &Product::name_);
}
Play @Compiler Explorer.
The exciting part is that printing in reverse order in the standard version requires to use rbegin/rend
iterators and then a custom unary function to print the exact data member from the Product
class. While with ranges we can apply views::reverse
, use a simple output function, and then a projection.
What’s missing is the parallel algorithm version of the ranges algorithms:
// standard:
std::for_each(std::execution::par, begin(prods), end(prods), /*...*/);
// no ranges version...
// std::ranges::for_each(std::execution::par, prods, /*... */); // doesn't compile...
Parallel versions are lacking for all ranges algorithms, not just for for_each
.
3. count_if
In the example below we’ll count Products that have name starting with “no”:
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
struct Product {
std::string name_;
double value_ { 0.0 };
};
int main() {
const std::vector<Product> prods {
{ "box", 10.0 }, {"tv", 100.0}, {"none", -1.0},
{ "car", 1000.0 }, {"toy", 40.0}, {"none", 0.0}
};
// standard version:
auto res = std::count_if(begin(prods), end(prods), [](const Product& p){
return p.name_.starts_with("no");
});
std::cout << "std::count_if: " << res << '\n';
// ranges version:
res = std::ranges::count_if(prods, [](const Product& p) {
return p.name_.starts_with("no");
});
std::cout << "std::ranges::count_if: " << res << '\n';
// alternative version for "none":
res = std::ranges::count(prods, std::string{"none"}, &Product::name_);
std::cout << "std::ranges::count: " << res << '\n';
}
Play @Compiler Explorer.
The example shows three approaches, and the last one uses a projection to check only the Product::name_
data member. In that approach, we search exactly for "none"
so it’s stricter than with starts_with
.
This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.
4. find_if
So far, our text algorithms have returned boolean or integral values, but with find*
functions, we have iterators (or subranges) that show the same occurrence.
See the example:
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
struct Product {
std::string name_;
double value_ { 0.0 };
};
int main() {
const std::vector<Product> prods {
{ "box", 10.0 }, {"tv", 100.0}, {"rocket", 10000.0},
{ "car", 1000.0 }, {"toy", 40.0}, {"none", 0.0}
};
// standard version:
auto it = std::find_if(begin(prods), end(prods), [](const Product& p){
return p.name_.starts_with("ro");
});
if (it != end(prods))
std::cout << "std::find_if: " << it->name_ << '\n';
// ranges version:
auto res = std::ranges::find_if(prods, [](const Product& p) {
return p.name_.starts_with("ro");
});
if (res != end(prods))
std::cout << "std::ranges::find_if: " << res->name_ << '\n';
}
Play @Compiler Explorer.
Like with many other algorithms, there’s also a “regular” version where you can pass two iterators:
it = std::ranges::find_if(begin(prods), end(prods), [](const Product& p) {
return p.name_.starts_with("ro");
});
The version that takes a single range is special, as it returns a borrowed iterators. This special type allows checking for temporary/lifetime object issues. This is not possible when you pass two iterators (because the container is present somewhere), but possible with a single temporary range:
struct Product {
std::string name_;
double value_ { 0.0 };
};
std::vector<Product> GetProds() {
return {
{ "box", 10.0 }, {"tv", 100.0}, {"rocket", 10000.0},
{ "car", 1000.0 }, {"toy", 40.0}, {"none", 0.0}
};
}
int main() {
auto it = std::ranges::find_if(GetProds(), [](const Product& p) {
return p.name_.starts_with("ro");
});
std::cout << "std::ranges::find_if: " << it->name_ << '\n';
}
This doesn’t compile and you’ll see the following error:
error: base operand of '->' has non-pointer type 'std::ranges::dangling'
22 | std::cout << "std::ranges::find_if: " << it->name_ << '\n';
| ^~
As you can see, the compiler checked that GetProds()
returns a temporary, and the iterator that we’d find would be dangling. See the code @Compiler Explorer.
5. find_first_of
Let’s have a look at another find*
function alternative that searchers for multiple elements at once.
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
struct Product {
std::string name_;
double value_ { 0.0 };
friend bool operator==(const Product& a, const Product& b) {
return a.name_ == b.name_ && abs(a.value_ - b.value_) < 0.0001;
}
};
int main() {
const std::vector<Product> prods {
{ "box", 10.0 }, {"default", 0.0 }, {"tv", 100.0}, {"rocket", 10000.0},
{ "car", 1000.0 }, {"toy", 40.0}, {"none", 0.0 }, { "ball", 40.0 }
};
const std::vector<Product> invalids {
{"default", 0.0 }, {"none", 0.0 }
};
// standard version:
auto it = std::find_first_of(begin(prods), end(prods), begin(invalids), end(invalids));
if (it != end(prods)) {
std::cout << "std::find_first_of: " << it->name_ << " at: "
<< std::distance(begin(prods), it) <<'\n';
auto it2 = std::find_first_of(std::next(it), end(prods), begin(invalids), end(invalids));
if (it2 != end(prods))
std::cout << "std::find_first_of: " << it2->name_ << " at: "
<< std::distance(begin(prods), it2) <<'\n';
}
// ranges version:
const std::array<std::string, 2> arrInvalids{"default", "none"};
auto res = std::ranges::find_first_of(prods, arrInvalids,
std::ranges::equal_to{}, &Product::name_);
if (res != end(prods)) {
const auto pos = std::distance(begin(prods), res);
std::cout << "std::ranges::find_first_of: " << res->name_
<< " at: " << pos <<'\n';
auto res2 = std::ranges::find_first_of(prods | std::views::drop(pos+1), arrInvalids,
std::ranges::equal_to{}, &Product::name_);
if (res2 != end(prods)) {
std::cout << "std::ranges::find_first_of: " << res2->name_
<< " at: " << std::distance(begin(prods), res2) <<'\n';
}
}
}
Play @Compiler Explorer.
std::find_first_of
takes two pairs of iterators. I wanted to find “invalid” products in my prod
sequence in the example. Since I’m comparing products, I had to define operator==
for my structure. Alternatively, I can provide a binary operation and then compare just the names:
auto cmpNames = [](const Product& a, const Product& b) {
return a.name_ == b.name_;
};
auto it = std::find_first_of(begin(prods), end(prods),
begin(invalids), end(invalids), cmpNames);
if (it != end(prods)) {
// ...
}
In the ranges version I can use projections and default comparator to acheive similar effect:
const std::array<std::string, 2> arrInvalids{"default", "none"};
auto res = std::ranges::find_first_of(prods, arrInvalids,
std::ranges::equal_to{}, &Product::name_);
The interesting part later is that for the second search I can use drop
to skip first N elements from the range:
auto res2 = std::ranges::find_first_of(prods | std::views::drop(pos+1),
arrInvalids, std::ranges::equal_to{}, &Product::name_);
Alternatively you can also use a version with two pairs of iterators:
auto res2 = std::ranges::find_first_of(std::next(res), end(prods),
begin(arrInvalids), end(arrInvalids),
std::ranges::equal_to{}, &Product::name_);
Would you like to see more?
To see more examples about projections and how to use them as a function argument, see this bonus article: "Utilities And Projections - C++17/C++20". It's available for C++ Stories Premium/Patreon members.
See all Premium benefits here.
6. mismatch
With the mismatch
algorithm we can find the first place where two ranges differ:
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
#include <iomanip> // quoted
int main() {
const std::string firstStr = "Hello Super World";
const std::string secondStr = "Hello Amazing World";
std::cout << "mismatch for " << std::quoted(firstStr)
<< " and " << std::quoted(secondStr) << '\n';
// standard version:
auto [first, second] = std::mismatch(begin(firstStr), end(firstStr), begin(secondStr));
{
const auto pos = std::distance(begin(firstStr), first);
std::cout << "std::mismatch: at pos " << pos << '\n';
}
// ranges version:
auto res = std::ranges::mismatch(firstStr, secondStr);
{
const auto pos = std::distance(begin(firstStr), res.in1);
std::cout << "std::ranges::mismatch: at pos " << pos << '\n';
}
}
Play @Compiler Explorer.
The ranges version returns:
template<class I1, class I2>
using mismatch_result = ranges::in_in_result<I1, I2>;
Which is a pair of two iterators, but we can access them via .in1
and .in2
.
Why not a simple range? At cpp reference we can see the following sentence:
Unlike
std::pair
andstd::tuple
, this class template has data members of meaningful names.
The result works fine with structured binding, so you can write:
auto [n1, n2] = std::ranges::mismatch(firstStr, secondStr);
const auto pos = std::distance(begin(firstStr), n1);
std::cout << "std::ranges::mismatch: at pos " << pos << '\n';
The code is almost the same as the standard version.
7. search
Searching for patterns in the other range/container:
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
#include <functional> // searchers
#include <iomanip>
int main() {
const std::string testString = "Hello Super World";
const std::string needle = "Super";
std::cout << "looking for " << std::quoted(needle)
<< " in " << std::quoted(testString) << '\n';
// standard version:
auto it = std::search(testString.begin(), testString.end(),
std::boyer_moore_searcher(needle.begin(), needle.end()));
if (it != testString.end()) {
const auto pos = std::distance(testString.begin(), it);
std::cout << "std::search: found at pos " << pos << '\n';
}
// ranges version:
auto res = std::ranges::search(testString, needle);
if (!res.empty()) {
const auto first = std::distance(testString.begin(), res.begin());
const auto last = std::distance(testString.begin(), res.end());
std::cout << "std::ranges::search: found between "
<< first << " and " << last << '\n';
}
}
Play @Compiler Explorer.
The standard version returns an iterator to the first string where the second string starts (or end()
if not there). While the ranges version returns a subrange (or a borrowed_subrange
).
We can also use projections for checking in a case insensitive way:
// ranges version:
const std::string testString2 = "hello abc world";
const std::string needle2 = "ABC";
std::cout << "looking for " << std::quoted(needle2) << " in "
<< std::quoted(testString2) << '\n';
res = std::ranges::search(testString2, needle2,
std::ranges::equal_to{}, ::toupper, ::toupper);
if (!res.empty())
{
const auto first = std::distance(testString2.begin(), res.begin());
const auto last = std::distance(testString2.begin(), res.end());
std::cout << "std::ranges::search: found between "
<< first << " and " << last << '\n';
}
Play @Compiler Explorer.
You can read more about searches in my two articles:
- Speeding up Pattern Searches with Boyer-Moore Algorithm from C++17 - C++ Stories
- Preprocessing Phase for C++17’s Searchers - C++ Stories
The other function ranges::search_n
is handy for finding N occurrences of a given value in the input range:
#include <algorithm>
#include <iostream>
#include <ranges>
#include <iomanip>
int main() {
const std::string sequence = "CTGCCCAGGGTTT";
const char letter = 'C';
const size_t count = 3;
std::cout << "looking for " << count << " "
<< letter << "'s in " << std::quoted(sequence) << '\n';
// standard version:
auto it = std::search_n(begin(sequence), end(sequence), count, letter);
if (it != end(sequence))
{
const auto pos = std::distance(begin(sequence), it);
std::cout << "std::search_n: found at pos " << pos << '\n';
}
// ranges version:
auto res = std::ranges::search_n(sequence, count, letter);
if (!res.empty())
{
const auto first = std::distance(begin(sequence), res.begin());
const auto last = std::distance(begin(sequence), res.end());
std::cout << "std::ranges::search_n: found between "
<< first << " and " << last << '\n';
}
}
Play @Compiler Explorer.
In the standard version, there are no special searchers; you can only invoke it using parallel algorithms.
Summary
In this article, we covered seven different algorithm “types” in the category of non-modifying operations: checking some predicate on all/none/some elements, searching, finding, general iteration. In total, there were more than 10 different examples.
The ranges algorithms offer an easier way to pass the “whole” container - just one argument, rather than to iterators. They also allow for projections and have a way to detect iterators to a temporary range. They also have limitations, like the lack of advanced searchers or parallel execution mode.
Stay tuned for the second part, where we’ll discuss remaining operations like std::transform
, sorting, min/max, partitioning, numerics, and we’ll see what we’ll get soon in C++23.
Back to you
- What’s your favorite aspect of ranges algorithms?
- Have you tried them in your projects?
Share your opinion and experience in the comments below the article.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here:
Similar Articles:
- Three Benchmarks of C++20 Ranges vs Standard Algorithms
- 20 Smaller yet Handy C++20 Features
- Examples of 7 Handy Functions for Associative Containers in Modern C++
- Empty Base Class Optimisation, no_unique_address and unique_ptr
- Increased Complexity of C++20 Range Algorithms Declarations - Is It Worth it?