上个月我在优化一条数据解析链路,函数本身逻辑不复杂,就是从缓冲区里取数、过滤、做单位换算,最后聚合。代码用了 C++20 的 std::ranges 管道写法,views::filter 接 views::transform,看着非常清爽。结果一压测,这条链路的耗时比手写循环慢了近 40 倍,而且数据量越大差距越明显。我当时第一反应是编译器没优化好,后来一行行排查,才意识到问题出在我对“适配器缓存”的理解上——视图适配器默认是不缓存任何中间结果的,哪些计算会被重复执行、哪些不会,完全取决于迭代器和解引用的交互方式。这篇文章就把我这次排查学到的 std::ranges 适配器缓存机制完整梳理一遍,重点讲清楚 C++23 引入的 std::views::cache_latest 到底解决了什么问题、解决不了什么问题,以及什么场景下你应该直接物化而不是上缓存。
1. 算清视图的干活时间:谓词在自增中执行,转换函数在解引用中执行
要理解适配器缓存,第一步不是去看缓存适配器本身,而是把视图适配器的惰性求值模型彻底搞清楚。大部分人对 ranges 的误解都来源于一个朴素的直觉:写了一个管道,编译器就会像执行普通函数一样从头到尾把数据算一遍。但实际上,视图适配器构造的时候什么都不干,它只是把操作的描述保存下来。真正的计算发生在迭代器前进和解引用的过程中,而且不同的适配器在这两个环节干活的比重完全不同。
1.1 惰性求值的真相:operator* 和 operator++ 各自承担什么
先看一个最简单的 filter 加 transform 的管道:
cpp复制std::vector<int> data{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto pipeline = data
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
上面这段代码执行完,filter 的谓词一次都不会被调用,transform 的平方函数也一次都不会执行。它只是构造了一个 filter_view 套着一个 transform_view 的嵌套对象,没有任何求值动作。只有当你在 for 循环里遍历 pipeline,或者手动调用 begin() 拿到迭代器、用 `
