C++ 移动语义和 std::move
C++ 11 开始添加了移动语义,伴随着 std::move
,对于现代 C++ 有着极为深远的影响。
移动的思想是将对象的所有权进行传递,传统上基于指针的传递并不能满足所有需求:指针只能指向对象或者为空,而移动则是保留对象,将对象的内容设为空,这种方式使得 RAII 一定程度上脱离了原有的函数栈的粗糙控制,使对象本身的声明周期不改变的情况下,更灵活的利用内存。
移动构造和移动赋值
移动构造和移动赋值实际上原理非常简单:若对象是一个内置数据类型或者是由基本数据类型构成的一个类,代表着其所有内存都是在栈上申请的,此时移动构造,移动复制和普通构造,普通复制保持一致;如果对象中有数据存放在堆区,用指针进行管理,那么就将该指针交给新构造的对象,将旧对象的指针设置为空,其他必要的,在堆中申请内存的类型使用值复制。
之前的文章 C++ 左值与右值 简单研究了左值与右值,而移动语义则是让一个左值具有右值的性质:在对某个对象进行移动的过程中,该对象将被视为右值,此时使用一个左值来接收这个右值,并在允许的情况下清空原对象,即完成了对源对象的移动。
具体的实现的代码如下:
#include <utility>
#include <iostream>
class A
{
public:
A(int num) // 构造
{
this->num = new int(num);
}
A(A &x) // 复制构造
{
this->num = new int(*x.num);
}
A(A &&x) // 移动构造
{
this->num = x.num;
x.num = nullptr;
}
void operator=(A &&x) noexcept // 移动赋值
{
this->num = x.num;
x.num = nullptr;
}
void operator=(A &x) // 普通(复制)赋值
{
this->num = new int(*x.num);
}
~A() // 析构
{
delete (this->num);
}
private:
int *num;
};
int main()
{
A a{10};
A ab = std::move(a);
}
值得注意的是,移动构造函数的声明为 A(A &&x)
,其中 &&
是右值引用的标记,换句话说,移动构造函数接收一个类型为 A 的右值引用。
std::move
使用移动语义不光需要在类中实现一个移动构造函数,还要使用 std::move
这个模板,在上面的代码中已经展示了 std::move
的用法。
std::move
的实现非常简单,实际上就只有一条语句:
template<class T>
constexpr std::remove_reference_t<T>&&
move(T&& t) noexcept;
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
std::remove_reference
std::remove_reference
的实现如下:
template <class T>
using remove_reference_t = typename remove_reference<T>::type;
template <class T>
struct remove_reference
{
using type = T;
};
template <class T>
struct remove_reference<T &>
{
using type = T;
};
template <class T>
struct remove_reference<T &&>
{
using type = T;
};
std::remove_reference_t
是一个别名模板,用于简化模板的书写。
std::remove_reference
唯一的作用就是得到解除引用后的原本的数据的类型,换句话说,传入 int&
和 int&&
时返回 int
,传入 int
时也返回 int
。
回过头来看 std::move
实现中的 return static_cast<typename std::remove_reference<T>::type&&>(t);
,你会发现这句话的意思是解除 t
的引用,获得其类型,并将其变为右值引用,然后用 static_cast
将 t
转换为 t
的类型的右值引用。
以上内容可由下面的代码验证:
#include <iostream>
#include <utility> // std::move, std::forward
#include <type_traits> // std::is_same, std::remove_reference
template <class T1, class T2>
void print_is_same()
{
std::cout << std::is_same<T1, T2>() << std::endl;
}
int main()
{
std::cout << std::boolalpha;
print_is_same<int, int>();
print_is_same<int, int &>();
print_is_same<int, int &&>();
print_is_same<int, std::remove_reference<int>::type>();
print_is_same<int, std::remove_reference<int &>::type>();
print_is_same<int, std::remove_reference<int &&>::type>();
int a = 0;
int &c = a;
int &&b = std::move(a);
print_is_same<int&&, decltype(std::move(a))>();
print_is_same<int&&, decltype(std::move(b))>();
print_is_same<int&&, decltype(std::move(c))>();
}
如果一个类提供了移动构造或者移动赋值,并且在构造或者赋值时使用了 std::move,那么 static_cast 就会调用对应的具有移动语义的重载函数;如果一个类只能移动赋值,不能复制,那么 static_cast 会直接选择移动赋值。