为何std::from_range构造容器时,部分场景需用std::views::as_rvalue?
为什么在语句std::vector a{ std::from_range, std::vector{TA(1), TA(2), TA(3), TA(4), TA(5)} | std::views::as_rvalue }中,尽管右侧vector是右值,仍需使用std::views::as_rvalue来避免拷贝;而在语句std::vector a{ std::from_range, std::views::iota(1,6) | std::views::transform([](auto elem) {return TA(elem); }) }中,却无需使用std::views::as_rvalue?
注:已知最优写法为std::vector<TA> a{ std::from_range, std::views::iota(1,6) }(无拷贝移动),但希望了解std::from_range构造函数的工作机制。
代码示例
#include <iostream> #include <vector> #include <ranges> class TA { private: int value; public: TA(int _value) : value(_value) { std::cout << "Constructor TA, value = " << value << '\n'; } TA(const TA& a) { value = a.value; std::cout << "Copy constructor TA, value = " << value << '\n'; } TA(TA&& a) noexcept { value = a.value; std::cout << "Move constructor TA, value = " << value << '\n'; } ~TA() { std::cout << "Destructor TA, value = " << value << '\n'; } }; void rvalue_vector_test1() { std::vector a{ std::from_range, std::vector{TA(1), TA(2), TA(3), TA(4), TA(5)} }; } void rvalue_vector_test2() { std::vector a{ std::from_range, std::vector{TA(1), TA(2), TA(3), TA(4), TA(5)} | std::views::as_rvalue }; } void iota_test1() { std::vector a{ std::from_range, std::views::iota(1,6) | std::views::transform([](auto elem) {return TA(elem); }) }; } int main() { rvalue_vector_test1(); rvalue_vector_test2(); iota_test1(); }
问题解答
核心逻辑:std::from_range的行为本质
std::from_range构造的核心是遍历传入的range,将每个元素构造到目标容器的内存中,具体调用拷贝还是移动构造,完全取决于遍历range时拿到的元素的值类别:
- 拿到左值:调用拷贝构造
- 拿到右值(纯右值或亡值):优先调用移动构造,甚至可能被编译器优化为直接构造(省略移动)
场景1:右值vector为什么需要as_rvalue?
std::vector{TA(1), ...}是临时右值容器,但它作为range被遍历时,迭代器解引用返回的是左值引用(TA&)——这是因为vector的迭代器不管容器本身是不是右值,解引用都是指向容器内部元素的左值引用(容器是右值不代表内部元素是右值)。
所以rvalue_vector_test1中,std::from_range遍历这个vector时,每次拿到的都是TA的左值,只能调用拷贝构造,导致大量拷贝。
而std::views::as_rvalue的作用是把原range的元素包装成右值引用视图:遍历这个视图时,迭代器解引用返回的是TA&&(亡值),此时std::from_range会调用移动构造,避免拷贝。
场景2:iota+transform视图为什么不需要as_rvalue?
std::views::transform返回的视图,其迭代器解引用时会执行传入的lambda表达式:return TA(elem);。这个lambda返回的是一个临时TA对象(纯右值),而非指向已有对象的引用。
当std::from_range遍历这个视图时,拿到的是纯右值形式的TA,编译器会自动将其移动到vector的内存中(甚至通过RVO优化,直接在vector的内存里构造TA,连移动都省略),所以不需要额外的as_rvalue。
补充:最优写法的原理
std::vector<TA> a{ std::from_range, std::views::iota(1,6) }之所以没有拷贝和移动,是因为std::from_range支持直接构造优化:它会直接用range中的元素值(这里是int类型的1~5),在vector的内存空间中调用TA的TA(int)构造函数,完全跳过中间的临时对象,是效率最高的构造方式。
内容的提问来源于stack exchange,提问作者Pavel

