为何ranges::accumulate调用invoke时不传递std::move(init)?
解析accumulate.hpp中传递范围时init被std::move的原因
最近看到截至2018年3月17日accumulate.hpp的d5e9afc提交里的这段代码,发现传递范围时init会被std::move一次,咱们来拆解下背后的逻辑:
首先看范围版本的重载代码:
T operator()(Rng && rng, T init, Op op = Op{}, P proj = P{}) const { return (*this)(begin(rng), end(rng), std::move(init), std::move(op), std::move(proj)); }
这段代码会调用迭代器版本的重载:
T operator()(I begin, S end, T init, Op op = Op{}, P proj = P{}) const { for(; begin != end; ++begin) init = invoke(op, init, invoke(proj, *begin)); return init; }
为啥第一个重载里要对init用std::move?
核心目的是避免不必要的拷贝开销。当你调用范围版本的接口时,传入的init可能是临时对象,或者是你不再需要的左值对象——用std::move把它转换成右值引用后,迭代器版本的重载在接收init参数时,如果T类型支持移动构造,就会优先使用移动构造来初始化参数,而不是拷贝构造。这对std::vector、std::string这类拷贝成本极高的类型来说,能带来很明显的性能提升。
那第二个重载的循环里为啥不用std::move(init)?
原因很简单:循环里的init是一个有名字的左值对象,我们需要持续对它进行累加操作。如果这里把init用std::move转换成右值传递给invoke(op, init, ...),要么会匹配到op的右值重载(如果有的话),导致init的资源被转移走,后续循环就会操作一个空的对象;要么会破坏累加的语义——毕竟我们是要把新值合并到init里,而不是把init的资源移走再重新赋值。
另外补充一点:第二个重载最后返回init时,其实可以考虑用std::move(init),因为函数参数不属于局部对象,RVO(返回值优化)不一定能生效,用std::move可以触发移动构造返回值。不过原代码没这么写,可能是出于兼容性考虑,或者觉得收益有限。
内容的提问来源于stack exchange,提问作者sandthorn
相关产品推荐
相关产品推荐

