[C++]右值引用与移动语义
[C++]右值引用与移动语义
概述
C++11引入了库函数std::move,它可以避免不必要的复制操作,实现高效的资源转移;一般在使用在临时变量或STL容器上;其核心是利用以右值引用为参数的移动构造函数中实现的移动语义。
右值引用
右值是对应于左值的概念,下面是《C++ Primer Plus》对左值的定义:
左值是什么呢?左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。
在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字 const 之前的情况。现在,常规变量和 const 变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而 const 变量属于不可修改的左值。
所以右值就是可出现在赋值表达式右边,但不能对其应用地址运算符的值;包括字面常量(char*字符串除外)、表达式和返回值不是引用的函数。
int x = y = 5;
float && rref1 = 3.14f;
int && rref2 = x+y;
double && rref3 = std::sqrt(8.0);
将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。
std::cout << "rref1: " << rref1 << " addr: " << &rref1 << '\n';
// out: rref1: 3.14 addr: 00000017680FFBA4
移动构造函数
引入右值引用的主要目的之一是实现移动语义(move semantics)。
什么是移动语义:C++11之前的复制操作类似于vector<string> vct_copy1(vct),创建新对象并把数据复制一份,似乎很合理;但是在这种情况下:
vector<string> foo(const vector<string>& v){
vector<string> temp(v);
// iterate temp ...
return temp;
}
vector<string> vct_copy2(foo(vct));
//显然temp复制了vct的数据, 而vct_copy2又复制了temp的数据
是把数据复制一份还是直接将temp的数据的所有权转让给vct_copy呢?答案显然是后者,这种方法被称为移动语义。
如何让编译器知道什么时候需要移动语义呢?这就是右值引用彰显作用的地方:
class A {
public:
char* data; // data ptr
A(const char* str){
data = new char[strlen(str) + 1];
strcpy(data, str);
}
A(const A& other){
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
A(A && other){
data = other.data; // 窃取地址 pilfering 而不是深复制
other.data = nullptr; // 避免析构时double free
}
~A(){
delete[] data;
}
};
类A除了常规的复制构造函数,还定义了参数为右值引用的移动构造函数。编译器会自动根据实参是否是右值引用来选择要调用的构造函数;另外,也可以对operator=进行移动赋值重载。
STD::MOVE
如果能利用移动构造函数复制对不是右值引用的变量,岂不是大大节约了开销?C++提供了库函数实现强制移动:utility头文件中定义的std::move函数可以将变量强制转换为右值引用,类似于static_cast<T &&>:
B b1(std::move(b));
// 传入b1构造函数的参数为右值引用,若B类定义了移动构造函数就会调用
// 否则会调用复制构造函数,若都没有则不会通过编译
右值引用带来的主要好处并非是能够编写使用右值引用的代码,而是能够使用利用右值引用实现移动语义的库代码。STL类现在都有复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符。
STL容器使用move的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<std::string> words;
std::string s1 = "hello";
std::string s2 = "world";
// 使用std::move将字符串移动到vector中
words.push_back(std::move(s1));
words.push_back(std::move(s2));
for (const auto& word : words) {
std::cout << word << std::endl;
}
// 在移动之后,s1和s2的内容可能是未定义的,但它们仍然是合法状态
std::cout << "s1: " << s1 << std::endl;
std::cout << "s2: " << s2 << std::endl;
return 0;
}
输出:
hello
world
s1:
s2:
参考:
[^《C++ Primer Plus》]: 第六版,chapter18.2

浙公网安备 33010602011771号