首页 > 编程语言 >c++11标准右值引用, 移动语义和完美转发

c++11标准右值引用, 移动语义和完美转发

时间:2023-03-11 19:55:13浏览次数:47  
标签:11 右值 左值 c++ && forward MyClass 拷贝

0. 序言

学习自C++ Rvalue References Explained (thbecker.net)


1. 引入

1.1 拷贝间接资源

如果一个类的成员变量有指针, 例如

class MyClass{
public:
  T* element;
}

有两个MyClass变量a和b, 这时候执行

a = b;

Visual Studio抛出以下错误尝试调用已经删除的函数xxx, 仔细看, 发现它是缺省拷贝函数

这是因为缺省拷贝函数是对应成员的简单复制, 它无法对间接资源进行浅拷贝或是深拷贝

所以编译器要求你来定义拷贝函数

class MyClass{
public:
  
  MyClass(MyClass& mc02){
		if (element != nullptr){
      delete element;
      element = nullptr;
    }
    
    // 复制资源, 而不是简单赋值, 如element = mc02.element
    element = new T(mc02.element);
  }
  
  T* element;
}c

简单小结:

一般拷贝函数的流程是

​ (1) delete origin resource

​ (2) copy target resource


1.2 可以不拷贝的情况

1.1中为什么要拷贝资源, 本质是为了让两个变量保持独立, 修改其中一个不会影响另一个, 那什么时候可以不拷贝, 根据前面拷贝资源的理由的, 可以想到如果只有一个变量, 就不需要拷贝多一份资源, 而用原来的就好. 看下面代码

T foo();

T a = foo();

等价为

T foo();

T b = foo();  // 如果你确定变量b除了下面赋值会使用一次, 以后都不会使用. 那么它可以省略.
T a = b;

foo函数返回了一个临时对象T, a = foo()这条语句执行, 依然是拷贝了这个临时对象的资源将它交赋值给a. 显然效率更高的做法是交换a和临时对象T的资源, 这样做少了拷贝资源的过程.

简单小结:

拷贝临时对象(变量)的一般流程

​ (1) swap two resource pointer


2. 左值和右值

这里的左值右值概念不是准确的(就像文章说的一样), 但是用来理解move语义已经够了.

定义

​ 能在赋值号两边的为左值

​ 只能在赋值号右边的为右值

另一种定义

​ 需要保持独立性的为左值

​ 临时的变量为右值(只使用一次)

int a, b;
// 10 is rvalue
a = 10;
10 = a;  // Error

// b is lvalue
a = b;
b = a;

int foo();
foo() = 20;  // Error

int& foo02();
foo02 = 100;  // OK

3. 移动语义

前面说到了左值右值, 以及对于变量和临时对象(变量)应该采取不同的赋值方式. 可以说对于左值,使用拷贝的方式; 对于右值,采用交换资源的方式, 对于左值右值, 有完全不一样的逻辑. 显然一个拷贝函数已经不够用了

右值引用构造函数

class MyClass{
public:
  	// 传入右值时, 调用这个拷贝函数
	  MyClass(MyClass&& mc02) noexcept {
				swap two resource
    }
  
  	// 传统的左值引用拷贝函数. 传入左值时, 调用这个拷贝函数
  	MyClass(MyClass& const mc02){
				delete origin resource
        copy mc02 resource
        assign to this resource 
    }
}
MyClass a;
MyClass foo();

MyClass b(a);  // execute MyClass(MyClass& const mc02)
MyClass c(foo());  // execute MyClass(MyClass&& mc02)

3.1 if have a name rule

MyClass& foo(MyClass&& other) noexcept {  // 传入的是右值
  MyClass a(other); // however in here. other is lvalue
  // question: which copy construction will be called. lvalue or rvalue reference.
}

答案是左值引用拷贝会被调用. 可能你会问, 命名传入的参数类型为MyClass&&

其实other是左值引用还是右值引用, 不是根据&数量来判断的, 区分的一个好方法是有名字的为左值引用, 没有名字的为右值引用

之所以这样定义, 因为有名字意味着还可能被使用, 需要保持独立性; 没名字的你想用也用不了了, 只能用一次

MyClass foo02(){
  return MyClass();
}

foo02() // 想想看为什么foo02返回的是右值, 因为它没名字

3.2 move函数

move函数允许把一个左值当作右值使用

如果不想理解原理, 记住move是通过套个函数外壳, 将一个有名字的左值, 返回为一个没名字的右值

为了连续调用右值拷贝, 加上move

MyClass& foo(MyClass&& other) noexcept {
  MyClass a(std::move(other));
}

3.2.1 move原理

move源码

template <class _Ty>
constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept {
    return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

remove_reference_t<_Ty> 作用是将模板参数_Ty去引用, 比如MyClass&变为MyClass

static_cast<&&>静态转换为右值引用

bb

template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;  // 看不懂

template <class _Ty>
struct remove_reference {
    using type                 = _Ty;
    using _Const_thru_ref_type = const _Ty;
};

3.3 move应用举例swap函数

swap源码

_void swap(_Ty& _Left, _Ty& _Right) noexcept(
    is_nothrow_move_constructible_v<_Ty>&& is_nothrow_move_assignable_v<_Ty>) {
    _Ty _Tmp = _STD move(_Left); 
    _Left    = _STD move(_Right);
    _Right   = _STD move(_Tmp);
}

一般swap实现

_void swap(_Ty& _Left, _Ty& _Right){
    _Ty _Tmp = _Left; 
    _Left    = _Right;
    _Right   = _Tmp;
}

右值交换一次拷贝都不进行,而左值复制则需要拷贝3次


4. 完美转发

4.0 问题

下面的函数模板有明显错误, 因为调用factory是通过值转递

template <typename T, typename ARG>
shared_ptr<T> factory(ARG arg){
	return shared_ptr<T>(new T(arg));  //ps: shared_ptr<T>不知道什么作用
}

应该修改为

template <typename T, typename ARG>
shared_ptr<T> factory(ARG& arg){  // 引用传递
	return shared_ptr<T>(new T(arg));
}

但上面只支持传入左值, 但有时候, 想要传入右值到函数模板, 比如

factory<MyClass, int>(10);

可以改为

template <typename T, typename ARG>
shared_ptr<T> factory(ARG& const arg){
	return shared_ptr<T>(new T(arg));
}

但这样就不能够修改arg


4.1 解决方案

统一修改为右值引用, 并使用一套模板实例规则

template <typename T, typename ARG>
shared_ptr<T> factory(ARG&& const arg){
	return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

c++ 11规定了一套&变化规则

& &表示&

& &&表示&

&& &表示&

&& &&表示&&

其中

传入左值时, 如MyClass&, ARG&&MyClass& &&等价为MyClass&

传入右值时, 如MyClass&&, ARG&&为如MyClass&& &&等价为MyClass&&

写ARG&&是为了避免值传递


4.1.1 std::forward的作用

&变化规则, 使得函数模板可以支持传入左值或右值

但if have a name rule导致代码里arg恒为左值

不做处理的话new T(arg)将恒调用T的左值拷贝函数

这显然不合理, 正确情况是, 传入左值模板参数, 应该调用T的左值拷贝函数

传入右值模板参数, 应该调用T的右值拷贝函数

std::forward的作用就是根据ARG是左值还是右值, 来转换左值arg为对应的左值或右值

std::forward源码

template <class _Ty>
constexpr _Ty&& forward(
    remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");  // forward只接收左值
  	return static_cast<_Ty&&>(_Arg);
}

_Ty为左值MyClass&, 变为

template <class _Ty>
constexpr MyClass& && forward(
    MyClass& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<MyClass& &&>(_Arg);
}

等价

template <class _Ty>
constexpr MyClass& forward(
    MyClass& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<MyClass&>(_Arg);
}

_Ty为右值MyClass&&, 变为

template <class _Ty>
constexpr MyClass&& && forward(
    MyClass& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<MyClass&& &&>(_Arg);
}

等价

template <class _Ty>
constexpr MyClass&& forward(
    MyClass& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<MyClass&&>(_Arg);
}

5. Last but no least

// 右值拷贝函数和其它使用右值引用的函数记得加上noexcept修饰, 否则不调用
MyClass(MyClass&& mc02) noexcept {  //OK
  swap two resource
}

MyClass(MyClass&& mc02) { // Error don't execute forever
  swap two resource
}

6. 总结

(1)右值引用在拷贝构造函数, operator=赋值操作符上有应用. 它的作用是减少不必要资源的拷贝带来的时间花销, 通常是对临时对象资源的拷贝, 代替为资源交换.

(2)还有一些内容没写, 比如边界效应. 具体的参考上面网页吧

标签:11,右值,左值,c++,&&,forward,MyClass,拷贝
From: https://www.cnblogs.com/laijianwei/p/17206807.html

相关文章

  • C++从txt中读取矩阵
    1.分析给定一个txt数据,中间由空格分割,目标是读取数据,以便后续使用。由于不清楚数据大小,为了方便管理,采用vector容器作为存贮对象。   2.程序下面是读取的方法......
  • 一个网络和串口全双工通信的c++库
    欢迎指正概述该库是https://github.com/ZLMediaKit/ZLToolKit和https://github.com/itas109/CSerialPort的集合这是一个通信库,包括网络和串口通信网络包括:TCP客户端......
  • 2023-03-11 Java中的动态数组
    类似C++中的vector,动态数组需要满足以下功能增(insert)删(remove)改(set)查(get和contain)支持泛型自动扩容和缩容上面的实现实际相当于JDK标准库中的java.util......
  • 11、NFS-CSI网络存储、SC提供动态制备模板 PV和PVC动态制备
    PV和PVC在Pod级别定义存储卷有两个弊端◼卷对象的生命周期无法独立于Pod而存在◼用户必须要足够熟悉可用的存储及其详情才能在Pod上配置和使用卷PV和PVC可用于降低这种耦......
  • Keil MDK6要来了,将嵌入式软件开发水平带到新高度,支持跨平台(2023-03-11)
    注:这个是MDK6,不是MDK5AC6,属于下一代MDK视频版:https://www.bilibili.com/video/BV16s4y157WF一年一度的全球顶级嵌入式会展EmbeddedWorld2023上,MDK6将展示预览版效......
  • 第 1 章 C++编程基础 Basic C++ programming
    1.1如何撰写C++程序_HowtoWriteaC++Program练习1.4,在终端上让用户输入fastname和lastname并打印出来练习1.4#include<iostream>#include<vector>#include......
  • ES6-ES11 ES11 BigInt
    原视频<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><title......
  • ES6-ES11目录
    ES6-ES11let变量声明以及声明特性ES6-ES11let实践案例ES6-ES11const声明常量以及特点ES6-ES11变量的解构赋值ES6-ES11模板字符串ES6-ES11对象的简化写法ES6-ES......
  • ES6-ES11 ES11绝对全局对象globalThis
    原视频忽略变量环境,引用全局变量html<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width......
  • ES6-ES11 ES11String.prototype.matchAll
    原视频<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><title......