右值和移动
值类别(value categories)lvalue通常可以放在等号左边的表达式, 左值例子变量函数或数据成员的名字返回左值引用的表达式如x, x 1, cout ’ . x 1 和 x返回的都是对x的int. x则返回的是int字符串字面量如 “hello world”rvalue通常只能放在等号右边的表达式, 右值glvaluegeneralized lvalue, 广义左值xvalueexpiring lvalue, 将亡值prvaluepure rvalue, 纯右值值类型(value type)是与引用类型(reference type)相对而言表明一个变量是代表实际数值还是引用另一个数值在C里所有的原生类型枚举结构联合类都代表值类型。只有引用()和指针( * ) 才是引用类型在Java里数字等原生类型是值类型类则属于引用类型在Python里一切类型都是引用类型引用类型(reference value)非常左值引用可以绑定非 const lvalue常左值引用可以绑定 lvalue, xvalue, prvalue非常右值引用可以绑定非 const xvalue, prvalue常右值引用可以绑定 xvalue, prvalue生命周期C的规则是一个临时对象(prvalue)会在包含这个临时对象的完整表达式估值完成后按生成顺序的逆序被销毁除非有生命周期延长发生C对临时对象有特殊的生命周期延长规则如果一个prvalue 被绑定到一个引用上它的生命周期则会延长到跟这个引用变量一样长result r process_shape( circle(), triangle());这条生命周期延长规则只对prvalue有效而对xvalue无效移动的意义右值引用的目的是实现移动而实现移动的意义是减少运行的开销移动案例string result string(Hello, ) name .;C11 以前的执行流程调用构造函数 string(const char*)生成临时对象 1Hello, 复制 1 次调用 operator(const string, const string)生成临时对象 2Hello, 复制 2 次name 复制 1 次调用 operator(const string, const char*)生成对象 3“Hello, 复制 3 次name 复制 2 次”. 复制 1 次假设返回值优化能够生效最佳情况对象 3 可以直接在 result 里构造完成临时对象 2 析构释放指向 string(Hello, ) name 的内存临时对象 1 析构释放指向 string(Hello, ) 的内存C11 以后的执行流程调用构造函数 string(const char*)生成临时对象 1Hello, 复制 1 次调用 operator(string, const string)直接在临时对象 1 上面执行追加操作并把结果移动到临时对象 2name 复制 1 次调用 operator(string, const char*)直接在临时对象 2 上面执行追加操作并把结果移动到 result“.” 复制 1 次临时对象 2 析构内容已经为空不需要释放任何内存临时对象 1 析构内容已经为空不需要释放任何内存如何实现移动除了拷贝构造函数外增加移动构造函数增加swap成员函数实现与另一个对象的快速交换在你的对象的名空间下应当有一个全局的 swap 函数调用成员函数 swap 来实现交换。支持这种用法会方便别人包括你自己在将来在其他对象里包含你的对象并快速实现它们的 swap 函数实现通用的 operator上面各个函数如果不抛异常的话应当标为 noexcept。这对移动构造函数尤为重要noexcept告诉编译器函数不会抛异常有利于编译器对程序做更多的优化以下情形鼓励用noexcept 移动构造函数move constructor、移动分配函数move assignment、析构函数destructor不要返回本地变量的引用在C编程中如果在函数内部返回一个指向本地对象的引用这是一个常见的错误int getLocalVariable() { int num 42; return num; // 错误返回了一个指向本地变量的引用 } int main() { int ref getLocalVariable(); // 错误引用指向已被销毁的本地变量 cout ref endl; // 可能输出任意值因为引用指向已被销毁的内存 }引用坍缩又称“引用折叠”当一个泛型函数模板中的类型参数与实际参数类型相同时编译器会进行类型折叠即将模板中的类型参数替换为实际参数类型从而简化代码。template typename T void process_shape(T shape) { puts(Processing shape); } void foo() { process_shape(circle()); // 调用 process_shape(circle()) process_shape(triangle()); // 调用 process_shape(triangle()) }当我们调用 process_shape(circle()) 和 process_shape(triangle()) 时编译器会根据实际参数类型进行类型折叠即 T 的推导结果分别是 circle 和 triangle在有模板的代码中当类型推导结果相同时编译器会将模板中的类型参数替换为实际参数类型从而简化代码。这个现象在实际编程中经常会遇到完美转发定义一个函数模板该函数模板可以接收任意类型参数然后将参数转发发给其他目标函数且保证目标函数接受的参数其类型与传递给模板函数的类型相同对于一个函数如果形参是右值引用但在函数体内这个“右值引用”实际上是一个左值变量然后函数内再有一个函数传入这个参数那么就会调用对应的左值引用版本而完美转发的意义就相当于做一次类型转换让这个参数保持一开始传入时的左值右值类别移动案例#include string #include ctime #include chrono #include iostream void func1(std::string s) { } void func2(const std::string s) { } void test2() { std::string a hello; auto start std::chrono::system_clock::now(); for (size_t i 0; i 20000000; i) { func1(std::string(hello)); } auto end std::chrono::system_clock::now(); std::chrono::durationdouble elapsed_seconds end - start; std::cout elapsed time: elapsed_seconds.count() s\n; start std::chrono::system_clock::now(); for (size_t i 0; i 20000000; i) { func2(std::string(hello)); } end std::chrono::system_clock::now(); elapsed_seconds end - start; std::cout elapsed time: elapsed_seconds.count() s\n; } // 改进版本 void test3() { std::string a hello; auto start std::chrono::system_clock::now(); for (size_t i 0; i 20000000; i) { func1(a); } auto end std::chrono::system_clock::now(); std::chrono::durationdouble elapsed_seconds end - start; std::cout elapsed time: elapsed_seconds.count() s\n; start std::chrono::system_clock::now(); for (size_t i 0; i 20000000; i) { func2(a); } end std::chrono::system_clock::now(); elapsed_seconds end - start; std::cout elapsed time: elapsed_seconds.count() s\n; } int main() { std::cout ---- test2 测试时间 ---- std::endl; test2(); std::cout ---- test3 测试时间 ---- std::endl; test3(); }结果:---- test2 测试时间 ---- elapsed time: 1.8719s elapsed time: 1.90104s ---- test3 测试时间 ---- elapsed time: 0.424229s elapsed time: 0.0873127s问题: 在test2 case 中为什么func1比func2快func1(string(“func1”))时传入的参数是一个右值.同时在函数调用中临时对象的所有权会被转移给函数的参数s因此会发生一次移动构造操作func2(string(“func2”))时传入的参数也是一个右值虽然函数参数s是通过常量引用传递的但临时对象string(“func2”)可以绑定到常量引用因此不会发生拷贝操作。为什么test3整体时间比test2快?例如func1(a)和func2(a)传入的参数是一个左值因为a是一个已命名的变量它具有持久性并且可以被多次使用. 原因在于使用已命名的变量作为参数时可以直接进行拷贝构造而不需要进行额外的临时对象的创建和销毁参考资料C右值引用