I noticed that I can seemingly get the same logic(copy elements matching some predicate into vector) both using ranges::copy_if and also using vector constructor taking 2 iterators(by providing it with filter_view .begin() and .end()).
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
#include <utility>
#include <fmt/ranges.h>
const std::vector<int> vals{1,2,3,47,8472};
const auto filter_pred = [](const int i){return i%2==0;};
void fna(){
std::vector<int> result;
std::ranges::copy_if(vals, std::back_inserter(result), filter_pred);
std::cout << fmt::format("{}", result) << std::endl;
}
void fn1(){
auto filtered = vals | std::views::filter(filter_pred);
std::vector<int> result {filtered.begin(), filtered.end()};
std::cout << fmt::format("{}", result) << std::endl;
}
std::vector<int> fna_val(){
std::vector<int> result;
std::ranges::copy_if(vals, std::back_inserter(result), filter_pred);
return result;
}
std::vector<int> fn1_val(){
auto filtered = vals | std::views::filter(filter_pred);
return {filtered.begin(), filtered.end()};
}
int main(){
fna();
fn1();
std::cout << fmt::format("{}", fna_val()) << std::endl;
std::cout << fmt::format("{}", fn1_val()) << std::endl;
}
Is there a reason to prefer one over other? What I can think of:
copy_if means destination size is unknown.copy_if could be faster to compile since it does not require <ranges> header, but did not benchmarkcopy_if requires mutable destination container, range construction does notstd::vector in body, {} works. Would be nice if vector understood ranges so we would not have to use .begin() .end(), but it does not.Just re-copying our comparison:
std::vector<int> fna_val(){
std::vector<int> result;
std::ranges::copy_if(vals, std::back_inserter(result), filter_pred);
return result;
}
std::vector<int> fn1_val(){
auto filtered = vals | std::views::filter(filter_pred);
return {filtered.begin(), filtered.end()};
}
First things first. Never write this:
return {filtered.begin(), filtered.end()};
For types where brace-initialization does something special (like vector!), it is simply asking for trouble to use brace-initialization in a context where you actively do not want the special brace-initialization behavior. Because:
vector<int> v = {filtered.begin(), filtered.end()};
vector w = {filtered.begin(), filtered.end()};
This does what you want for v, but w is a vector of two filter_view<...>::iterators. That's pretty subtle. So I would strongly encourage avoiding ever doing this — because it is so easy to get wrong.
That said, if we consider how the vector initialization actually works if you spell it correctly:
ranges::copy_if version is basically doing push_back in a loop. That means that you will do some number of allocations, roughly the log2 of the number of elements you're pushing back (depends on the growth factor). So for 100 elements, that could be 7 allocations.views::filter version will do one single allocation, which it will figure out by walking the entire range twice — once to count how many elements are in it, and then once to actually read them.Which is better? It depends on many factors — how many elements you have, how expensive the filter predicate is (the more expensive it is, the worse it would be to invoke it twice per element), and how high the percentage of elements that satisfy the predicate is. For the latter, consider that the copy_if version could start by simply doing:
result.reserve(vals.size());
That ensures that we have exactly 1 allocation. And if like 70% of our elements satisfy filter_pred, that's probably a good trade-off. If like 1% of our elements satisfy filter_pred, that allocation is going to be much bigger than it should be. Maybe that's bad.
So... it depends. But please don't use brace-initialization unless you actually want brace-initialization.
Note that in C++23, the filter version would be better written like
return vals
| std::views::filter(filter_pred)
| std::ranges::to<std::vector>();
Which additionally avoids the question of how to specifically spell that initialization.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With