为什么需要移动语义
对于一个自定义的类,该类拥有一些资源,比如堆中的连续数据
struct Vec {
Vec(int size):
size(size),
data(new int[size])
{}
~Vec(){delete[] data;}
Vec(const Vec& v) {
size = v.size;
data = new int[size];
memcpy(data, v.data, size);
}
Vec& operator=(const Vec& v) {
if(this == &v) return *this;
size = v.size();
delete[] data;
data = new int[size];
memcpy(data, v.data, size);
return *this;
}
int* data;
int size;
};
这个类的语义是掌管了一份资源,这个资源可以深拷贝给其他对象,但每个对象拥有的资源是不同且唯一的,不可共享。
大部分情况,这种语义是OK的,但在某些需求下希望一个对象的资源的所有权是可以转移给别人的,转移了之后自己就不再拥有这份资源。
那么就应当这样实现拷贝和赋值
Vec(Vec-Type v) {
size = v.size;
data = v.data;
v.data = nullptr;
v.size = 0;
}
Vec& operator=(Vec-Type v) {
if(this == &v) return *this;
delete[] data;
size = v.size();
data = v.data;
v.data = nullptr;
v.size = 0;
return *this;
}
此处的Vec-Type用于区别const Vec&, 这样就能根据传入的参数类型来决定调用拷贝还是移动。
在c++11前,一般使用Boost中rvref来实现
template<typename T>
struct Boost_rvref {
T& ref;
};
template<typename T>
Boost_rvref<T> move(const T& in) {
return in;
}
Vec(Boost_rvref<Vec> v) {
size = v.ref.size;
data = new int[size];
memcpy(data, v.ref.data, size);
}
Vec a(3);
Vec b(move(a));
确定是需要更多的使用move来触发立即数和将亡值的移动
Vec(const Vec&) = delete;
Vec f() {
Vec v(3);
return v;
}
Vec b(f()); // 会调用Vec(const Vec&) 报错
c++11后官方给出了rvalue ref类型,来区分拷贝和移动。
Vec(Vec&& v) {
size = v.ref.size;
data = new int[size];
memcpy(data, v.ref.data, size);
}
Vec a(3);
Vec b(std::move(a));
所以右值引用的唯一作用就是实现移动构造和移动赋值
对比boost这种模板的方式,官方的标准支持立即数和将亡值的移动,而不需要额外套一个move
左值和右值
类型 和 左右值是两个不同维度的东西,调用函数时的函数签名既包含类型也包含左右值, 两个都对应才能调用
左值:能取出地址,内存中实际存在,有一个标识符(名称)的值
右值:不能取出地址,通常是临时对象
左值和右值只是语法层面的东西,只在编译期发挥作用,指挥编译器选取对应函数
右值引用是一种类型,绑定到一个右值上。
右值引用可以是左值也可以是右值
A a;
A&& a1 = std::move(a);
// a1和std::move(a)类型都是A&&, 但a1是左值, std::move(a)是右值
通用引用和完美转发
来源
// 右值引用
A&& a1 = A();
// 通用引用
auto&& a2 = A();
template<typename T>
void f(T&& param);
通用引用其实是effective c++的作者提出, 用来描述引用折叠效果的概念
引用折叠是c++标准的编译规则
只有T&& && 才是右值,其他组合方式都是左值
struct A {
A() = default;
};
void f(const A&){std::cout << "const A&\n";}
void f(A&&){std::cout << "A&&\n";}
template<typename T>
void test(T&& t) {
f(t);
}
A a;
test(a); // const A&, t的类型是 && + & = &
test(std::move(a)); // const A&, t的类型是&& + && = &&, 但是t是个左值
std::forward
为了实现传进来的参数是左值,t就是左值;传进来的参数是右值,t就是右值,需要加std::forward
template<typename T>
void test(T&& t) {
f(std::forward<T>(t));
A a;
test(a); // const A&
test(std::move(a)); // A&&
}
具体实现
// 针对传入的是左值
template <typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
// 针对传入的是右值
template <typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
static_assert(!std::is_lvalue_reference<T>::value, "T must not be an lvalue reference");
return static_cast<T&&>(t);
}
可以发现forward也利用了引用折叠的规则,传入左值 && + & = &, 传入右值 && + && = &&
// f(std::forward<T>(t)) 相当于
if t is &, f(t)
if t is &&, f(std::move(t))
应用
实现输入类T的构造参数,返回T的智能指针
template<typename T, typename... Args>
std::shared_ptr<T> make_shared(Args&&... args){
// args是Args&&...类型的左值
// 所以下面调用的是T的拷贝构造
return std::shared_ptr<T>(new T(args...));
}
改为
template<typename T, typename... Args>
std::shared_ptr<T> make_shared(Args&&... args){
// 完美转发 args的左/右值属性
// 传入的是左值就转发左值
// 传入的是右值就转发右值
return std::shared_ptr<T> (new T(std::forward<Args>(args)...);
}