过滤函数开销大时,如何处理std::ranges过滤视图?
std::views::filter中昂贵谓词被重复调用 当执行类似
std::ranges::to<std::vector>(vals | std::views::filter(expensive));的代码时,expensive谓词会被调用两次:一次是std::vector构造函数通过std::distance(beg, end)计算元素数量以预分配内存,另一次是实际遍历范围构造元素时。
针对你提出的思路分析
1. 让过滤视图表现为std::ranges::input_range
可以实现。std::views::filter默认返回的是forward_range(若源范围为forward_range),如果让它降级为input_range,std::ranges::to就无法提前计算元素数量,只能通过back_inserter逐个插入,从而避免两次遍历。
实现方式是给过滤视图包裹一层仅满足input_range要求的适配器:比如自定义一个适配器,让其迭代器仅实现input_iterator的接口(不支持多遍遍历、仅允许与std::default_sentinel比较operator==),这样范围会被识别为input_range,触发std::ranges::to的单遍插入逻辑。
2. 直接使用std::ranges::copy + back_inserter
这是最直接有效的方案。无论是否预先为result预留空间,这种方式只会遍历范围一次,expensive仅被调用一次。如果尝试手动统计数量再预留空间,反而会再次调用expensive,得不偿失,因此直接用back_inserter逐个插入是这类场景的最优选择之一。
3. 其他可行方案
- 提前缓存过滤结果:如果源数据不会变更,可以先将过滤后的元素存入中间容器(如
std::vector),后续直接使用该容器。但该方案不适用于一次性流数据。 - 自定义带缓存的过滤适配器:实现一个类似
filter的适配器,在第一次遍历时同时统计元素数量并缓存所有符合条件的元素,之后一次性分配内存构造目标容器。这种方式需要自行管理缓存,适合频繁处理昂贵过滤逻辑的场景。
关于实现std::views::expensive_filter的合理性
完全合理。这个适配器的核心是让返回的范围仅满足std::ranges::input_range,从而禁用所有依赖forward_range的预计算逻辑(比如std::ranges::to的内存预分配),强制采用单遍遍历方式,从根源上避免昂贵谓词被重复调用。
实现时需注意:
- 适配器的迭代器仅满足
input_iterator要求,不支持多遍遍历 - 过滤逻辑与
std::views::filter保持一致,仅限制范围的迭代器类别
内容的提问来源于stack exchange,提问作者Ben

