分析C++ std::move的作用,以及它如何影响智能指针所有权

在介绍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具体是通过下面几个步骤来实现的:

  1. 删除左值拷贝构造函数
  2. 删除左值赋值运算符函数
  3. 重载右值拷贝构造函数
  4. 重载右值赋值运算符函数

它们都实现了右值拷贝构造函数右值赋值运算符,这将会在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;
}
相关推荐
程序猿进阶5 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
闻缺陷则喜何志丹10 分钟前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
charlie11451419122 分钟前
C++ STL CookBook
开发语言·c++·stl·c++20
小林熬夜学编程33 分钟前
【Linux网络编程】第十四弹---构建功能丰富的HTTP服务器:从状态码处理到服务函数扩展
linux·运维·服务器·c语言·网络·c++·http
倔强的石头10644 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
A懿轩A2 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
机器视觉知识推荐、就业指导2 小时前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香2 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Ronin3053 小时前
11.vector的介绍及模拟实现
开发语言·c++
✿ ༺ ོIT技术༻3 小时前
C++11:新特性&右值引用&移动语义
linux·数据结构·c++