首页 > 编程语言 >关于c++的右值引用

关于c++的右值引用

时间:2023-07-16 11:23:42浏览次数:56  
标签:右值 int 左值 c++ 引用 && foo

引用

A reference can be thought of as a name of an object.

  • 左值引用: 引用左值.

  • 右值引用: 引用右值, 用于参数传递, 函数返回值, 表达式中间结果. 类似于常量引用, 但是可以被修改.

(左值)引用类型的变量只能绑定到左值表达式, 只有const引用可以绑定到右值.
右值引用类型的变量只能绑定到右值表达式.

左值引用很好理解也经常用到, 右值引用还有点陌生, 接下来详细聊一聊.
引入右值引用是为了解决什么问题? 引入后又带来的什么新的问题? 新的问题又是怎么解决的?

引入右值引用是为了解决什么问题?

为了弥补C++在移动语义上的缺失. 在右值引用出现之前, 我们在函数调用传参时, 只有两种语义: 给它一份拷贝(按值传递), 或者给它一份引用(按引用传递).

那移动语义又是什么玩意儿? 为什么需要移动语义?

其实就是移动构造和移动赋值运算符, 把一个对象持有的资源转移给另一个新的对象, 避免先拷贝再释放资源的问题. 区别于已有的拷贝构造和赋值.

至于为什么需要, 无外乎是实践中有相应的使用场景.
比如std::unique_ptr, 它不能支持拷贝构造, 因为std::unique_ptr的语义就是要独占对象所有权.
这样一来函数要想返回一个std::unique_ptr就做不到了, 因为函数返回的时候要要构造一个新的std::unique_ptr, 自然就会涉及到拷贝构造.
有了移动语义就方便多了, 把原来的std::unique_ptr上的对象所有权转移到一个新的std::unique_ptr上并返回. 原来的std::unique_ptr随着函数结束自然销毁.

class unique_ptr {
public:

    unique_ptr(unique_ptr&& _Right) noexcept // 移动构造
    {
    }

    template <class _Dx2 = _Dx, enable_if_t<is_move_assignable_v<_Dx2>, int> = 0>
    unique_ptr& operator=(unique_ptr&& _Right) noexcept // 移动赋值
    {
        if (this != _STD addressof(_Right)) {
            reset(_Right.release());
            _Mypair._Get_first() = _STD forward<_Dx>(_Right._Mypair._Get_first());
        }
        return *this;
    }

    unique_ptr(const unique_ptr&) = delete;

    unique_ptr& operator=(const unique_ptr&) = delete;
};

引入后又带来的什么新的问题?

先看个例子, 下面代码中的foo(rref)会调到哪个实现foo会调到哪个重载?

void foo(int &)  { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }

int main() {
  int &&rref = 1;
  foo(rref);    // output: lvalue
}

答案是foo(int &), 是不是有点无法理解? 接着往下看.

foo(int &)接受一个左值类型的形参, 根据左值引用的规则, 左值引用类型的变量只能绑定左值. 所以, 要想匹配这个重载, 调用者必须传一个左值. 比如foo(a)

foo(int &&)接受的是一个右值类型的形参, 根据右值引用的规则, 右值引用类型的变量只能绑定右值表达式. 所以, 要想匹配这个重载, 调用者必须传一个右值, 比如foo(99).

这很好理解, 神奇的地方在rref这个变量. rref的类型是int&&(右值引用), 绑定的值的类别是右值, 但有意思的是, 这个变量本身却是一个左值.

所以, 按照上面的重载匹配规则, 自然就匹配到了foo(int &).

这也就是引入右值引用后带来的问题:
变量类型值类别, 变成了两个概念. 和左值引用表达式只能是左值不同, 右值引用类型的表达式既可以是左值也可以是右值.
重载匹配的时候右值引用类型的实参被当成了左值来用. 导致语义上出现错误.

新的问题又是怎么解决的?

标准库中提供了个std::move函数:

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

其实就是一个类型转换, 把传入的实参_Arg转换成一个右值.

所以, 上面的foo应该这样调:

int main() {
  int &&rref = 1;
  foo(std::move(rref));
  }

完美转发

上面讲的是最基础的场景, 接下来再看看和模板结合的场景:

void foo(int &)
{
    std::cout << "lvalue" << std::endl;
}

void foo(int &&)
{
    std::cout << "rvalue" << std::endl;
}

template<typename T>
int bar(T&& x)
{
    foo(x);
}

从语义上看, 我们希望给bar传一个左值时能够转调到foo的左值引用版本(foo(int&)),
bar传一个右值时能够转调到foo的右值引用版本(foo(int&&)).

但是bar的形参x本身就是一个左值, 所以无论怎么传只能匹配到foo的左值引用版本, 显然达不到我们想要的目的.
这时候就需要完美转发了, 将函数实参以其原本的值类别转发出去. 标准库中提供了个std::forward函数.

所以上面的bar可以这么写:

template<typename T>
int bar(T&& x)
{
    foo(std::forward<T>(x));
}

扒一扒std::forward源码:

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

template <class _Ty>
constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept
{
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call"); // _Ty不能是左值引用类型
    return static_cast<_Ty&&>(_Arg);
}

其实就是一个静态类型转换, 传入左值的时候, 会匹配第一个重载, 这个重载根据_Ty的类型将实参转发为左值或右值. 传入右值时, 会匹配第二个重载, 这个重载根据_Ty的类型将实参转发为右值.
需要注意的是模板参数_Ty无法由编译器推断, 必须显式指定.

第一个重载不是很好理解, 补充一下展开过程:

  • 模板参数传入int展开:
int && forward(int& _Arg) 
{
    return static_cast<int &&>(_Arg); // 按右值转发
}
  • 模板参数传入int&展开:
int& && forward(int& _Arg) 
{
    return static_cast<int& &&>(_Arg); // 引用折叠, 折叠为int&, 按左值转发
}
  • 模板参数传入int&&展开:
int&& && forward(int& _Arg)
{
    return static_cast<int&& &&>(_Arg); // 引用折叠, 折叠为int&&, 按右值转发
}

这里其实涉及到引用折叠转发引用两个概念, 再补充说明一下:

o 引用折叠:

模板编程中参数类型推导出现双重引用时, 双重引用将被折叠成一个引用, 要么是左值引用, 要么是右值引用.
折叠规则是: 如果任一引用为左值引用, 则结果为左值引用, 否则(即两个都是右值引用), 结果为右值引用.

rvalue reference to rvalue reference collapses to rvalue reference, all other combinations form lvalue reference.

o 转发引用:
转发引用就是对一个待推导的类型 T 应用上右值引用的形式(T&&)。编译器在做类型推导时,会将绑定左值的T推导为左值引用类型,将绑定右值的T推导为原基本类型,再叠加引用坍缩规则后,绑定左值的变量的实际类型为左值引用,绑定右值的变量的实际类型为右值引用。

参考文献:

Everything about Rvalue Reference: https://www.zhihu.com/question/363686723/answer/1976488046
c++11的移动语义和完美转发: https://zhuanlan.zhihu.com/p/398817111

标签:右值,int,左值,c++,引用,&&,foo
From: https://www.cnblogs.com/snail-0304/p/17557600.html

相关文章

  • c++类型转换与RTTI运行阶段类型识别
    我们都知道C++完全兼容C语言,C语言的转换方式很简单,可以在任意类型之间转换,但这也恰恰是缺点,因为极其不安全,可能不经意间将指向const对象的指针转换成非const对象的指针,可能将基类对象指针转成了派生类对象的指针,这种转换很容易出bug,需要严格审查代码才能消除这种隐患,但是C这种转换......
  • lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by /home/liuj
     glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于glibc囊括了几乎所有的 UNIX 通行的标准,可以想见其内容包罗万象。而......
  • C++ lambda函数
    一、格式大括号内是变量捕获,小括号内是参数列表。变量捕获的来源都是当前位置“能看得见”的变量。二、变量捕获上图左:将它“能看得见”的变量都按引用捕获。上图右:将它“能看得见”的变量都按值捕获。比如某个lambda表达式在语句块内,但是它全体引用捕获的话可以捕获全局......
  • VS2015 C++代码内存泄漏
    在可能泄漏的文件头部加入下面几行(保证malloc和new的泄漏都可以检测到)#define_CRTDBG_MAP_ALLOC#include<crtdbg.h>#ifdef_DEBUG//重载new运算符#definenewnew(_NORMAL_BLOCK,__FILE__,__LINE__)#endif程序运行起始位置:_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_C......
  • 关于为IAM用户添加KMS权限-可以完成对EC2开关机以及创建EBS时引用KMS的权限策略
    在AWS中,从创建磁盘,或者从快照中创建EBS磁盘时,都可以选择指定的KMS加密这样IAM用户就必须得有KMS相关的权限,可以在IAM中添加策略、也可以在KMS中进行策略的添加这里笔者主要讲述在KMS的policy中,如何添加,可以将如下的json写入到密钥策略中Statement字段中{"Sid":"Allowus......
  • 105.C++初始化
    105.C++初始化C++中变量的初始化有很多种方式,如:默认初始化,值初始化,直接初始化,拷贝初始化,列表初始化。1.默认初始化默认初始化是指定义变量时没有指定初值时进行的初始化操作。默认初始化变量的值与变量的类型与变量定义的位置有关系:1.1内置类型变量对于内置类型变量(如in......
  • Grpc C++使用总结
    Grpc一,安装安装依赖sudoapt-getinstallpkg-configsudoapt-getinstallautoconfautomakelibtoolmakeg++unzipsudoapt-getinstalllibgfalgs-devlibgtest-devsudoapt-getinstallclanglibc++-dev下载gRPCgitclonehttps://github.com/grpc/grpc.gitc......
  • c++ day 9
    今天来学习选择排序选择排序有多种方法下面是方法一:选择排序(SelectionSort)是一种简单但低效的排序算法。它的基本思想是在未排序的部分中选择最小(或最大)的元素,并将其放置在已排序部分的末尾。通过重复这个过程,直到所有元素都排好序为止。下面是选择排序的C++实现示例:#incl......
  • C/C++学生宿舍管理系统[2023-07-14]
    C/C++学生宿舍管理系统[2023-07-14]课程名称:程序设计实践专业班级:学生姓名:学号:任课教师:张闻强学期:2022-2023学年第2学期课程报告任务书题目 学生宿舍管理系统主要内容 用C语言开发一个简单的学生宿舍管理系统。实现宿舍......
  • 【ChernoC++笔记】智能指针
    【44】【ChernoC++】【中字】C++的智能指针智能指针(Smartpointers)是C++中的一种特殊类型,用于管理动态分配的内存资源。智能指针通过封装指针,并在适当的时机自动释放内存,从而避免内存泄漏和悬空指针等常见问题。unique_ptr❓为什么叫做uniqueptr?unique_ptr不能复制:如果复......