在介绍std::move之前,首先需要说明C++中左值右值相关的概念。
基本概念
左值(lvalue)
左值是可以标识内存位置的表达式,通常是有名字的变量或对象。左值表示一个可修改的数据,你可以对其取地址。例如,变量、数组元素、结构体成员等都是左值。
c++
int x = 13; // x 是左值
int* ptr = &x; // &x 返回一个左值
右值(rvalue)
右值是不具有标识内存位置的临时表达式,通常是计算的结果或临时对象。右值表示临时的、一次性的数据,不能对其取地址。例如,字面值、临时对象、表达式的计算结果等都是右值。
C++
int result = 5 + 3; // 5 + 3 是右值
int* ptr = &x + 4; // &x + 4 是右值
(左值)引用和右值引用
引用(Reference): 引用是一个别名,它引用(关联)到已经存在的对象。左值引用可以绑定到左值,而右值引用可以绑定到右值。
C++
int x = 13;
int& lvalueRef = x; // 左值引用
int&& rvalueRef = 5 + 3; // 右值引用
std::move的作用
C++
template <class _Tp>
_LIBCPP_NODISCARD_EXT inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR __libcpp_remove_reference_t<_Tp>&&
move(_LIBCPP_LIFETIMEBOUND _Tp&& __t) _NOEXCEPT {
typedef _LIBCPP_NODEBUG __libcpp_remove_reference_t<_Tp> _Up;
return static_cast<_Up&&>(__t); // 将__t强制转换为右值
}
std::move的作用是将左值转换为右值 ,它在一般情况下其实并没有什么作用,它是用于实现特殊语意的方式,通过重载右值相关的符号或者构造函数来实现的,对于std::unique_ptr和std::shared_ptr这些智能指针用处就很大。
它可以用来确保std::unique_ptr的所有权转移只能通过std::move来实现,普通的赋值运算符无法使用,也无法通过已有的std::unique_ptr对象来直接初始化一个新的std::unique_ptr对象(但是实际上可以重载普通的左值构造函数和左值赋值运算符也实现相同的效果,类似rust语言自动转移所有权)。 std::unique_ptr具体是通过下面几个步骤来实现的:
- 删除左值拷贝构造函数
- 删除左值赋值运算符函数
- 重载右值拷贝构造函数
- 重载右值赋值运算符函数
它们都实现了右值拷贝构造函数 和右值赋值运算符,这将会在std::move强制转换成右值赋值时被调用,他们都会将原地址转移给新的智能指针,同时将原本智能指针存储的地址释放掉。
std::unique_ptr的实现
C++
__compressed_pair<pointer, deleter_type> __ptr_;
unique_ptr(unique_ptr const&) = delete; // 删除左值拷贝构造函数
unique_ptr& operator=(unique_ptr const&) = delete; // 删除左值赋值运算函数
// 右值拷贝构造函数
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_SINCE_CXX23 unique_ptr(unique_ptr&& __u) _NOEXCEPT
: __ptr_(__u.release(), _VSTD::forward<deleter_type>(__u.get_deleter())) {}
// 右值赋值运算符
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_SINCE_CXX23 unique_ptr& operator=(unique_ptr&& __u) _NOEXCEPT {
reset(__u.release());
__ptr_.second() = _VSTD::forward<deleter_type>(__u.get_deleter());
return *this;
}
std::shared_ptr的实现(没有删除任何构造函数,其左值拷贝构造函数具有特殊的作用------添加引用计数)
c++
// 左值拷贝构造函数
_LIBCPP_HIDE_FROM_ABI
shared_ptr(const shared_ptr& __r) _NOEXCEPT
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_)
{
if (__cntrl_)
__cntrl_->__add_shared();
}
// 左值赋值运算符重载
_LIBCPP_HIDE_FROM_ABI
shared_ptr<_Tp>& operator=(const shared_ptr& __r) _NOEXCEPT
{
shared_ptr(__r).swap(*this);
return *this;
}
// 右值拷贝构造函数
_LIBCPP_HIDE_FROM_ABI
shared_ptr(shared_ptr&& __r) _NOEXCEPT
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_)
{
__r.__ptr_ = nullptr;
__r.__cntrl_ = nullptr;
}
// 右值赋值运算符重载
_LIBCPP_HIDE_FROM_ABI
shared_ptr<_Tp>& operator=(shared_ptr&& __r) _NOEXCEPT
{
shared_ptr(_VSTD::move(__r)).swap(*this);
return *this;
}
std::unique_ptr通过std::make_unique创建,std::shared_ptr通过std::make_shared创建。 创建后是一个位于栈上的变量,当程序执行离开这个栈帧的时候,该对象释放前会调用对应的析构函数。 对于std::unique_ptr来说,它会立刻将对应地址的对象释放掉。 对于std::shared_ptr来说,当每调用一次析构函数,其引用计数都会-1,当变成0后会被释放掉。
C++
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_SINCE_CXX23 ~unique_ptr() { reset(); }
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_SINCE_CXX23 void reset(pointer __p = pointer()) _NOEXCEPT {
pointer __tmp = __ptr_.first();
__ptr_.first() = __p;
if (__tmp)
__ptr_.second()(__tmp); // second是deleter_type
}
值得注意的是,在std::vector、std::map等STL容器中存储智能指针或对象时,如果发生了erase(包括替换某个元素),也将会调用它们的析构函数。
通过示例展现std::move的用处
下面创建了一个A类,并分别实现了左值右值的拷贝构造函数和重载了赋值运算符(其中左值相关函数,会改变原对象的内容。右值相关的函数,都做了将原对象的数据清空的操作。而std::unique_ptr则是将原对象指针和释放相关的函数转移到新的对象中。),并在main中调用了这些函数。 为了简化演示,这个A类管理的不是指针,而是两个int类型的数据。
C++
class A {
public:
A (int a, int b) {
this->a = a;
this->b = b;
}
// 左值拷贝构造函数
A (A& a) {
this->a = a.a;
this->b = a.b;
a.a = 100;
a.b = 150;
printf("operator lvalue copy \n");
}
// 重载左值赋值运算符函数
A& operator=(A& a) {
a.a = 200;
a.b = 300;
printf("operator lvalue = \n");
return *this;
}
// 右值拷贝构造函数
A (A&& a) {
this->a = a.a;
this->b = a.b;
a.a = 0;
a.b = 0;
printf("operator rvalue copy \n");
}
// 重载右值赋值运算符函数
A& operator=(A&& a) {
this->a = a.a;
this->b = a.b;
a.a = 0;
a.b = 0;
printf("operator rvalue = \n");
return *this;
}
int a;
int b;
};
int main() {
A *a = new A(5, 10);
printf("A: a - %d, %d\n", a->a, a->b);
// 创建新对象b,因此调用的是左值拷贝构造函数,而非左值赋值运算符
A b = *a; // operator lvalue copy
printf("B: a - %d, %d\n", a->a, a->b);
printf("C: b - %d, %d\n", b.a, b.b);
// 创建新对象d,并且使用了std::move函数强制将b转换为右值,因此调用右值拷贝构造函数
A d = std::move(b); // operator rvalue copy
printf("D: b - %d, %d\n", b.a, b.b);
printf("E: d - %d, %d\n", d.a, d.b);
// d已经创建出来了,此处将a对应的对象赋值给d,因此调用的是左值赋值运算符
d = *a; // operator lvalue =
printf("F: a - %d, %d\n", a->a, a->b);
// d已经创建出来了,此处通过std::move函数强制将a对应的对象转换为右值,并赋值给d,因此调用的是右值赋值运算符
d = std::move(*a); // operator rvalue =
printf("G: a - %d, %d\n", a->a, a->b);
printf("H: d - %d, %d\n", d.a, d.b);
return 0;
}
输出结果
bash
A: a - 5, 10
operator lvalue copy
B: a - 100, 150
C: b - 5, 10
operator rvalue copy
D: b - 0, 0
E: d - 5, 10
operator lvalue =
F: a - 200, 300
operator rvalue =
G: a - 0, 0
H: d - 200, 300
Program ended with exit code: 0
修改示例,使其仅运行通过std::move转移所有权,模拟std::unique_ptr指针
将上面的示例中,左值拷贝构造函数和左值赋值运算符都删除后,通过=直接完成初始化后后续赋值都将是不被允许的了,必须通过std::move将A对象转换成右值,才能进行赋值或初始化操作。此处右值拷贝构造函数和右值赋值运算符都会将原来的A对象中的内容转移到新的对象后,将其清空(修改为0)。
C++
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <set>
#include <vector>
#include <map>
class A {
public:
A (int a, int b) {
this->a = a;
this->b = b;
}
// 左值拷贝构造函数
A (A& a) = delete;
// 重载左值赋值运算符函数
A& operator=(A& a) = delete;
// 右值拷贝构造函数
A (A&& a) {
this->a = a.a;
this->b = a.b;
a.a = 0;
a.b = 0;
printf("operator rvalue copy \n");
}
// 重载右值赋值运算符函数
A& operator=(A&& a) {
this->a = a.a;
this->b = a.b;
a.a = 0;
a.b = 0;
printf("operator rvalue = \n");
return *this;
}
int a;
int b;
};
int main() {
A *a = new A(5, 10);
printf("A: a - %d, %d\n", a->a, a->b);
// 创建新对象b,因此调用的是左值拷贝构造函数,而非左值赋值运算符
//////////////
//////////////
A b = *a; ////////////// ### 报错:Call to deleted constructor of 'A'
printf("B: a - %d, %d\n", a->a, a->b);
printf("C: b - %d, %d\n", b.a, b.b);
// 创建新对象d,并且使用了std::move函数强制将b转换为右值,因此调用右值拷贝构造函数
A d = std::move(b); // operator rvalue copy
printf("D: b - %d, %d\n", b.a, b.b);
printf("E: d - %d, %d\n", d.a, d.b);
// d已经创建出来了,此处将a对应的对象赋值给d,因此调用的是左值赋值运算符
//////////////
//////////////
d = *a; ////////////// ### 报错:Overload resolution selected deleted operator '='
printf("F: a - %d, %d\n", a->a, a->b);
// d已经创建出来了,此处通过std::move函数强制将a对应的对象转换为右值,并赋值给d,因此调用的是右值赋值运算符
d = std::move(*a); // operator rvalue =
printf("G: a - %d, %d\n", a->a, a->b);
printf("H: d - %d, %d\n", d.a, d.b);
return 0;
}