移动语义

C++11引入了移动语义。其核心思想是通过移动构造函数转移,而不是复制资源,从而避免不必要的深拷贝,提升性能。

并且,一些具有独占所有权,或者因为其特性不可被复制的资源,只能通过移动的方式来在不同容器中转移资源。

例如 uniuqe_ptrthread对象只能通过移动构造函数完成资源的转移。

1
2
std::unique_ptr<int> a = std::make_unique<int>(42);
std::unique_ptr<int> b = std::move(a); // 所有权转移
1
2
std::thread t1([](){});
std::thread t2 = std::move(t1); // 执行权转移

移动构造函数是利用不同参数重载的构造函数。在创建对象时,传入一个类的右值引用就可以调用类中已经声明了的移动构造函数。

1
2
3
4
5
MyString(MyString&& other) noexcept 
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 置空原对象,防止资源释放
other.size_ = 0;
}

std::move可以将一个左值强制转化为右值引用,以调用类的移动构造函数。

万能引用

万能引用是C++11的新特性,它使得参数模板可以同时接收左值引用和右值引用,并且得知其原本的引用类型。

1
2
3
4
template<typename T>
void wrapper(T&& arg) {
// 后续操作
}

参数模板的类型T会保存其原有的引用类型,并且通过引用折叠,保留原有的引用类型。

引用折叠

当编译器遇到多层引用时,会按照以下规则将多层引用折叠为单层引用。

1
2
3
4
T &  &	→ T&
T & && → T&
T && & → T&
T && && → T&&

可以简单概括一下。只要存在左值引用,结果就是左值引用;只有全为右值引用时,结果才是右值引用。

注意,引用折叠一般只会发生在类型推导或者起别名时,手动实现多层引用是非法的。

通过引用折叠机制,编译器就可以得知传入的是什么类型的引用了。

完美转发

虽然引用折叠机制实现的万能引用已经可以保留左右值引用信息了,但是,对于需要区分左右值的函数来说,不论传入的是左值还是右值引用,它本身都是一个左值。

1
2
3
4
template<typename T>
void wrapper(T&& arg) {
Obj obj(arg);
}

在这个例子中,arg本身记录着左/右值引用的信息。但是,arg又是一个有名字的左值。所以构造对象obj时,一定不会调用其移动构造函数。这是因为移动构造函数的调用不是根据传入参数引用的类型而是而是实际类型调用的。

使用std::forward可以恢复参数原来的值类别。

1
2
3
4
template<typename T>
void wrapper(T&& arg) {
Obj obj(forward<T>(arg));
}