参考自:右值引用与转移语义(李胜利)
C++11之前,右值是不能被引用的,最大限度就是用常量引用绑定一个右值,如 :
const int& a = 1;
为了与左值引用区分,右值引用 && 表示。如下:
#include <iostream>
void fun(int& i) {
std::cout << "lvalue:" << i << std::endl;
}
void fun(int&& i) {
std::cout << "rvalue:" << i << std::endl;
}
int main() {
int a = 0;
fun(a);
fun(1);
return 0;
}
输出:
lvalue:0
rvalue:1
可以发现,临时变量 1 使用了入参为右值引用的 fun 函数完成了函数调用。
右值引用的出现解决了C++11之前移动对象效率问题。下面用自定义的String类来初识转移语义。首先定义一个不带转移语义的普通类:
#include <iostream>
#include <vector>
#include <string.h>
class String {
public:
String() {
std::cout << "String()" << std::endl;
};
String(const char* str) {
len_ = strlen(str);
Init(str);
std::cout << "String(const char*)" << std::endl;
}
String(const String& str) {
len_ = str.len_;
Init(str.data_);
std::cout << "String(const String&)" << std::endl;
}
String& operator= (const String& str) {
if (this != &str) {
len_ = str.len_;
delete data_;
Init(str.data_);
}
std::cout << "operator=" << std::endl;
}
~String() {
if (data_) { delete data_; }
std::cout << "~String()" << std::endl;
}
private:
void Init(const char* str) {
data_ = new char[len_ + 1];
memcpy(data_, str, len_);
data_[len_] = '\0';
}
char* data_ = nullptr;
uint32_t len_ = 0;
};
int main() {
String a;
a = String("hello");
std::vector<String> vec;
vec.push_back("world");
return 0;
}
输出:
String()
String(const char*) // 1
operator= // 2
~String()
String(const char*) // 3
String(const String&) // 4
~String()
~String()
~String()
上述代码中,String(“hello”) 和 String(“world”) 都是临时对象,也就是右值。整个过程中,我们实际只需要2个对象,即只需要2次内存分配即可,实际确是4次,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。
巧的是:C++11后 vector提供了 emplace_back() 可以就地构造对象,从而减少一次拷贝构造。不过对于 a = String("hello") 我们还得借助转移语义。
下面给我们为 String 添加移动构造函数及移动拷贝赋值函数。
#include <iostream>
#include <vector>
#include <string.h>
class String {
public:
String() {
std::cout << "String()" << std::endl;
};
String(const char* str) {
len_ = strlen(str);
Init(str);
std::cout << "String(const char*)" << std::endl;
}
String(const String& str) {
len_ = str.len_;
Init(str.data_);
std::cout << "String(const String&)" << std::endl;
}
String(String&& str) { // 移动构造函数
len_ = str.len_;
data_ = str.data_;
str.len_ = 0;
str.data_ = nullptr;
std::cout << "move String(const String&&)" << std::endl;
}
String& operator= (const String& str) {
if (this != &str) {
len_ = str.len_;
delete data_;
Init(str.data_);
}
std::cout << "operator=" << std::endl;
}
String& operator= (String&& str) { // 移动赋值函数
if (this != &str) {
len_ = str.len_;
delete data_;
data_ = str.data_;
str.len_ = 0;
str.data_ = nullptr;
}
std::cout << "move operator=" << std::endl;
}
~String() {
if (data_) { delete data_; }
std::cout << "~String()" << std::endl;
}
private:
void Init(const char* str) {
data_ = new char[len_ + 1];
memcpy(data_, str, len_);
data_[len_] = '\0';
}
char* data_ = nullptr;
uint32_t len_ = 0;
};
int main() {
String a;
a = String("hello");
std::vector<String> vec;
vec.push_back("world");
return 0;
}
输出如下:
String()
String(const char*) // 1
move operatro=
~String()
String(const char*) // 2
move String(const String&&)
~String()
~String()
~String()
原先的拷贝构造函数和拷贝赋值函数的调用被移动函数替代,减少了两次构造过程,节省了资源,提高了程序运行的效率。
几个注意点:
1. 参数(右值)的符号必须是右值引用符号,即“&&”。
2. 参数(右值)不可以是常量,因为我们需要修改右值。
3. 参数(右值)的资源链接和标记必须修改。否则, 右值的析构函数就会释放资源。转移到新对象的资源也就无效了。
标准库函数 std::move
编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,那么如何对左值使用移动函数,即把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。
#include <iostream>
void fun(int& i) {
std::cout << "lvalue:" << i << std::endl;
}
void fun(int&& i) {
std::cout << "rvalue:" << i << std::endl;
}
int main() {
int a = 0;
fun(std::move(a));
fun(1);
return 0;
}
输出如下:
rvalue:0
rvalue:1
std::move
在提高 swap 函数的的性能上非常有帮助,一般来说,swap
函数的通用定义如下:
template <classT> swap(T& a, T& b)
{
T tmp(a); // copy a to tmp
a = b; // copy b to a
b = tmp; // copy tmp to b
}
有了 std::move,swap 函数的定义变为 :
template <classT> swap(T& a, T& b)
{
T tmp(std::move(a)); // move a to tmp
a = std::move(b); // move b to a
b = std::move(tmp); // move tmp to b
}
通过 std::move,一个简单的 swap 函数就避免了 3 次不必要的拷贝操作。