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

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

原文链接:https://ovea-y.cn/cpp_move_analysis/

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

在介绍std::move之前,首先需要说明C++中左值右值相关的概念。

基本概念

左值(lvalue)

左值是可以标识内存位置的表达式,通常是有名字的变量或对象。左值表示一个可修改的数据,你可以对其取地址。例如,变量、数组元素、结构体成员等都是左值。

int x = 13;  // x 是左值
int* ptr = &x;  // &x 返回一个左值

右值(rvalue)

右值是不具有标识内存位置的临时表达式,通常是计算的结果或临时对象。右值表示临时的、一次性的数据,不能对其取地址。例如,字面值、临时对象、表达式的计算结果等都是右值。

int result = 5 + 3;  // 5 + 3 是右值
int* ptr = &x + 4;   // &x + 4 是右值

(左值)引用和右值引用

引用(Reference): 引用是一个别名,它引用(关联)到已经存在的对象。左值引用可以绑定到左值,而右值引用可以绑定到右值。

int x = 13;
int& lvalueRef = x;  // 左值引用
int&& rvalueRef = 5 + 3;  // 右值引用

std::move的作用

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的实现

__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的实现(没有删除任何构造函数,其左值拷贝构造函数具有特殊的作用——添加引用计数)

	// 左值拷贝构造函数
    _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后会被释放掉。

_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类型的数据。

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;
}

输出结果

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)。

#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;
}

原文链接:https://ovea-y.cn/cpp_move_analysis/

Read more

香港银行开户指南

香港银行开户指南

注意:本文所有内容,都是需要前往香港的情况下才能使用! 本文主要介绍2家实体银行和3家虚拟银行! 实体银行包含: * 中国银行(香港) * 汇丰银行(香港),该银行也被称为“香港上海汇丰银行” 虚拟银行包含: * 众安银行 * 天星银行 * 蚂蚁银行 1、各银行所需资料和办理方式 银行名称 办理方式 所需材料 & 要求 备注 汇丰银行(香港) 提前预约,线下到营业点办理 (如果没预约,只能很早排队取线下号,不一定能取到) 必备证件: 1. 港澳通行证 2. 入境海关小票 3. 身份证 投资证明: 1. 证券App近三月股票交割单 2. 中国结算近三月交割单 3. 银行近三月流水单 4. 支付宝投资流水 资产证明: 1. 银行资产证明 2.

By 夕元
C++协程(Coroutines)

C++协程(Coroutines)

原文链接:https://ovea-y.cn/cpp_coroutine_20/ C++协程(Coroutines) 1. 简单介绍协程 协程可以简单的理解为,它是一个可以随时“中断”,并再次恢复执行的函数。 C++协程框架的特点: * 无栈协程 * 非对称设计(开发者可以自行设计协程调度器,做成对称设计) 2. 协程和函数的区别 函数:函数调用是线性、同步、一次性的执行模式,调用者必须等待被调用方法执行完成后返回。 协程:协程可以异步执行,调用者不需要等待协程完成,可以在协程挂起时继续做其他事情。在执行过程中通过特定的语法(co__yield_和_co__await)暂停执行,并在之后的某个时刻恢复执行。 2.1 普通函数的执行过程 一个普通函数在执行的时候,主要包含两个操作,分别是调用(call)和返回(return)

By 夕元
GitHub Workflows

GitHub Workflows

原文链接:https://ovea-y.cn/github_workflows/ 本文介绍GitHub工作流的创建和作用。 工作流创建方式 在git项目下,创建.github/workflows文件夹,里面编写的任何yml文件都是工作流的一部分。 secrets的创建方式 secrets.GITHUB_TOKEN是GitHub自动创建的,不需要自己创建。 secrets.SERVER_HOST这个的来源,需要在GitHub对应的项目的设置中设置。 readme的构建标签 配置好Branch、Event类型后,把下面这段内容复制到markdown文档里,就会自动显示自动化流程状态了。其实就是一张图片,由GitHub自动生成提供。 一. 自动构建网站并推送到远程服务器 自动构建hugo,并更新远程服务器的静态网站内容。 自动化流程文件 name: Deploy Hugo Site # 当推送到main分支时触发部署流程 on: push: branches: - main

By 夕元
代码版本管理工具(git/gerrit/repo)

代码版本管理工具(git/gerrit/repo)

原文链接:https://ovea-y.cn/code_version_control_tools__git_gerrit_repo/ 一、版本控制工具的历史 1.1 版本控制雏形 在版本控制软件出现之前,就具备diff与patch工具来对源码进行比较和打补丁了,在CVS出来的一段时间里,Linus一直在使用diff与patch工具管理着Linux的代码。diff与patch也是源码版本控制中最基本的概念。 1.1.1 diff —— 用于比较两个文件或目录之间的差异 -u 表示使用 unified 格式 -r 表示比较目录 -N 表示将不存在的文件当作空文件处理,这样新添加的文件也会出现在patch文件中 diff -urN a.c b.c > c.patch 1.1.2 patch —— 用于应用差异修改 通过patch可以将原始文件变成目标文件,

By 夕元