【C++ 学习 ㉚】- 超详解 C++11 的右值引用

news2024/11/26 21:18:25

目录

一、左值和右值

二、左值引用和右值引用

三、移动语义

3.1 - 左值引用的短板

3.2 - 移动构造和移动赋值

四、move 函数

4.1 - remove_reference 的实现

4.2 - move 的实现

五、完美转发

5.1 - forward 的实现

5.2 - 使用场景


 


很多初学者都感觉右值引用晦涩难懂,其实不然,右值引用只不过是一种新的 C++ 语法,真正理解起来有难度的是基于右值引用引申出来的 2 种 C++ 编程技巧,分别为移动语义和完美转法


一、左值和右值

C++ 中左值和右值的概念是从 C 语言中继承过来的,而我在学习 C 语言的过程中,也接触到了左值和右值,并通过下面的一道练习题对它们的概念做了详细的了解

《C 陷阱与缺陷》练习 1-4:a+++++b 的含义是什么

上式唯一有意义的分解方式是 a ++ + ++ b,但根据"大嘴法"规则,上式应该被分解为 a ++ ++ + b,不过这个式子从语法上来说是不正确的,它等价于 ((a++)++) + b其中 a++ 的结果不能作为左值,编译器不会接受 a++ 作为后面的 ++ 操作符的操作数

术语"符号"(token)指的是程序中的一个基本组成单元,其作用相当于一个句子中的单词。

将程序分解成一个一个符号的过程,称为"词法分析",编译器中处理这一过程的部分,一般称为"词法分析器"。

词法分析中的"贪心法"

某些符号,例如 /、* 和 =,只有一个字符,称为单字符符号;而其他符号,例如 /*、== 以及标识符,包括了多个字符,称为多字符符号。当编译器读入一个字符 / 后又跟了一个字符 *,那么编译器必须做出判断,是将其作为两个分别的符号对待,还是合起来作为一个符号对待。解决方案可以归纳为一个很简单的规则:每一个符号应该包含尽可能多的字符,也就是说,编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是下一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串不再可能组成一个有意义的符号。这个处理策略有时被称为"贪心法",或者更口语化一点,被称为"大嘴法"

需要注意的是,除了字符串与字符常量,符号的中间不能嵌有空白(空格符、制表符和换行符)

所以,如果 / 是为了判断下一个符号而读入的第一个字符,而 / 之后紧接着 *,那么无论上下文如何,这两个字符都将被当成一个符号 /*,表示一段注释的开始。

y = x/*p;

上面语句的本意是用 x 除以 p 所指向的值,把所得的商再赋给 y,但实际上,/* 被编译器理解为一段注释的开始,编译器将不断地读入字符,直到 */ 出现。将上面的语句重写:

y = x/ *p;

或者更清楚一点,写作:

y = x/(*p);

在 C 语言或者 C++ 中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景的不同,分为左值表达式和右值表达式。通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法:

  1. 左值既能够出现在等号左边,也能出现在等号右边;右值则只能出现在等号右边

    int a = 10;  // 变量 a 是一个左值,而字面量 5 是一个右值
    int b = a;  // a、b 都是左值,只不过可以将 a 当做右值使用
  2. 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值

    // &a++;  // error
    &++a;  // ok

    a++ 是先复制一份临时的数据出来参与周围环境的运算,再让 a 自加 1,临时的数据没有固定的地址,所以 a++ 无法作为左值

    ++a 则是先让 a 自加 1,再将其放到周围环境中参与运算,所以 a++ 可以作为左值

注意:左值的英文简写为 "lvalue",右值的英文简写为 "rvalue",很多人可能会认为它们分别是 "left value" 和 "right value" 的缩写,其实不然,"lvalue" 是 "locator value" 的简写,指的是存储在内存中、有明确存储地址(可寻址)的数据,而 "rvalue" 是 "read value" 的简写,指的是可以提供数据值的数据(不一定可以寻址,例如存储在寄存器中的数据)


二、左值引用和右值引用

C++98/03 标准中就有引用,使用 "&" 表示。但这种引用方式有一个缺陷,即只能引用左值,无法引用右值,因此 C++98/03 标准中的引用又称为左值引用

int a = 10;
int& ra = a;  // ok
// int& r = 10;  // error

虽然 C++98/03 标准不支持为右值建立左值引用,但允许使用 const 左值引用来引用右值,也就是说,const 左值引用既可以引用左值,也可以引用右值

int a = 10;
const int& ra = a;  // ok
const int& r = 10;  // ok

我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式,这样就产生了一个问题,实际开发中我们可能对右值进行修改(实现移动语义时就需要),显然 const 左值引用的方式是行不通的

为此,C++11 标准引入了另一种新的引用方式,称为右值引用,用 "&&" 表示

注意:和左值引用一样,右值引用也必须进行初始化,且只能使用右值进行初始化

#include <iostream>
using namespace std;
​
int main()
{
    // int a = 10;
    // int&& rra = a;  // error(不能使用左值初始化右值引用)
    int&& rr = 10;  // 使用右值进行初始化
    rr = 100;  
    cout << rr << endl;  // 100
    return 0;
}

最后,C++ 语法上是支持 const 右值引用的,例如

const int&& a = 10;

但这种定义出来的右值引用并无实际用处,一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,const 右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给 const 左值引用完成


三、移动语义

3.1 - 左值引用的短板

左值引用的使用场景是做函数的参数以及返回值,这样可以避免拷贝对象,提高效率,但当函数的返回值是一个局部对象(出了函数作用域就会被销毁),就不能使用左值引用返回,只能按值返回

string.h

#pragma once
​
#include <cassert>
#include <cstring>
#include <iostream>
​
namespace yzz
{
    class string
    {
    public:
        string(const char* str = "")
        {
            assert(str);  // 前提是 str 非空
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            memcpy(_str, str, _size + 1);
        }
​
        // 拷贝构造函数(实现深拷贝)
        string(const string& s)
            : _size(s._size)
            , _capacity(s._capacity)
            , _str(new char[s._capacity + 1])
        {
            std::cout << "string(const string& s)" << std::endl;
            memcpy(_str, s._str, _size + 1);
        }
​
        ~string()
        {
            if (_str)
            {
                delete[] _str;
                _str = nullptr;
                _size = _capacity = 0;
            }
        }
​
        // 赋值运算符重载(利用上面写好的拷贝构造函数实现深拷贝)
        void swap(string& s)
        {
            std::swap(_str, s._str);
            std::swap(_size, s._size);
            std::swap(_capacity, s._capacity);
        }
​
        string& operator=(const string& s)
        {
            std::cout << "string& operator=(const string& s)" << std::endl;
            if (this != &s)
            {
                string tmp(s);
                swap(tmp);
            }
            return *this;
        }
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}

test.cpp

#include "string.h"
​
yzz::string func()
{
    yzz::string s("hello world");
    return s;
}
​
int main()
{
    yzz::string ret1 = func();
    // string(const string& s)
    cout << "----------------------------" << endl;
​
    yzz::string ret2;
    ret2 = func();
    // string(const string& s) 
    // string& operator=(const string & s)
    // string(const string & s)
    return 0;
}

 

3.2 - 移动构造和移动赋值

在 yzz::string 类中添加移动构造函数和移动赋值函数后,就可以解决函数按值返回时效率不高的问题

// 移动构造
string(string&& rr)
    : _str(nullptr), _size(0), _capacity(0)
{
    std::cout << "string(string&& rr)" << std::endl;
    swap(rr);
}
​
// 移动赋值
string& operator=(string&& rr)
{
    std::cout << "string& operator=(string&& rr)" << std::endl;
    swap(rr);
    return *this;
}

C++11 标准对右值做了更加细致的划分,分别称为纯右值(Pure value,简称 pvalue)和将亡值(eXpiring value,简称 xvalue)。其中纯右值指的是内置类型的右值,而将亡值则指的是自定义类型的右值

注意

  1. string(const string& s)string(string&& rr) 构成函数重载,且不会出现调用不明确的情况。string& operator=(const string& s)string& operator=(string&& rr) 同理。例如

    #include <iostream>
    using namespace std;
    ​
    void func(const int& r) { cout << "void func(const int& r)" << endl; }
    ​
    void func(int&& rr) { cout << "void func(int&& rr)" << endl; }
    ​
    int main()
    {
        int a = 10;
        func(a);  // void func(const int& r)
    ​
        func(10);  // void func(int&& rr)
        return 0;
    }

    所以在 yzz::string 类中添加移动构造函数和移动赋值函数后,使用该类的右值对象初始化同类对象或为同类对象赋值时,编译器会调用移动构造函数 string(string&& rr) 和移动赋值函数 string& operator=(string&& rr)

  2. 无论是左值引用,还是右值引用,它们本身都是左值

    #include <iostream>
    using namespace std;
    ​
    void test(int& r) { cout << "左值引用" << endl; }
    ​
    void test(int&& r) { cout << "右值引用" << endl; }
    ​
    int main()
    {
        int a = 10;
        int& ra = a;
        cout << &ra << endl;  // ok
    ​
        int&& rr = 10;
        cout << &rr << endl;  // ok
    ​
        test(ra);  // 左值引用
        test(rr);  // 左值引用
        return 0;
    }

    所以在移动构造函数和移动赋值函数中调用 swap 函数(参数类型为左值引用)是没问题的


四、move 函数

move 本意为 "移动",但该函数并不能移动任何数据,仅仅只执行强制类型转换,无条件地将实参强制转换为右值

在理解 move 是如何实现之前,需先理解 remove_reference 是如何实现的

4.1 - remove_reference 的实现

template<class T>
struct remove_reference
{
    typedef T type;
};
​
// 特化版本(偏特化)
template<class T>
struct remove_reference<T&>
{
    typedef T type;
};
​
template<class T>
struct remove_reference<T&&>
{
    typedef T type;
};

remove_reference 的作用是去除 T 中的引用部分,只获取其中的类型部分

示例

#include <type_traits>
#include <iostream>
​
int main()
{
    typedef std::remove_reference<int>::type A;
    typedef std::remove_reference<int&>::type B;
    typedef std::remove_reference<int&&>::type C;
​
    std::cout << std::is_same<int, A>::value << std::endl;  // 1
    std::cout << std::is_same<int, B>::value << std::endl;  // 1
    std::cout << std::is_same<int, C>::value << std::endl;  // 1
    return 0;
}

4.2 - move 的实现

万能引用(Universal Reference):如果模板(包括类模板和函数模板)中的函数参数写成 T&&,那么函数既可以接受右值,也能接受左值

#include <iostream>
using namespace std;
​
template<class T>
void func(T&& arg)
{
    cout << std::is_same<int, T>::value << " ";
    cout << std::is_same<int&, T>::value << " ";
    cout << std::is_same<int&&, T>::value << endl;
​
    cout << std::is_same<int, T&&>::value << " ";
    cout << std::is_same<int&, T&&>::value << " ";
    cout << std::is_same<int&&, T&&>::value << endl;
}
​
int main()
{
    func(10);
    // 1 0 0
    // 0 0 1
    cout << "--------" << endl;
    int a = 10;
    func(a);
    // 0 1 0
    // 0 1 0
    return 0;
}

注意:C++ 不允许对引用再进行引用,所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用

引用折叠(Refence Collapse)的规则是:如果任一引用为左值引用,则结果为左值引用;否则,即两个都是右值引用,结果才是右值引用。总结

  1. T& & 变成 T&

  2. T& && 变成 T&

  3. T&& & 变成 T&

  4. T&& && 变成 T&&

move 的实现

template<class T>
typename remove_reference<T>::type&& move(T&& arg) noexcept
{
    return static_cast<typename remove_reference<T>::type&&>(arg);
}

示例

#include "string.h"
#include <utility>
​
int main()
{
    yzz::string s1("hello world");
    yzz::string s2 = std::move(s1);
    return 0;
}

 


五、完美转发

5.1 - forward 的实现

转发左值

template<class T>
T&& forward(typename remove_reference<T>::type& arg) noexcept
{
    return static_cast<T&&>(arg);
}

T&& 可能会发生引用折叠,当 T 被推导为左值引用,则 T& && 为 T&;当 T 被推导为右值引用,则 T&& && 为 T&&

转发右值

template<class T>
T&& forward(typename remove_reference<T>::type&& arg) noexcept
{
    return static_cast<T&&>(arg);
}

示例

#include <utility>
#include <iostream>
using namespace std;
​
void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const 左值引用" << endl; }
​
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const 右值引用" << endl; }
​
template<class T>
void PerfectForward(T&& t)
{
    // Func(t);  // (1) 注意:无论是左值引用,还是右值引用,它们都是左值
    Func(std::forward<T>(t));  // (2) 完美转发
}
​
int main()
{
    PerfectForward(10);
    // 1、左值引用
    // 2、右值引用
​
    int a;
    PerfectForward(a);            
    // 1、左值引用
    // 2、左值引用
​
    PerfectForward(std::move(a)); 
    // 1、左值引用
    // 2、右值引用
​
    const int b = 8;
    PerfectForward(b);  
    // 1、const 左值引用
    // 2、const 左值引用
​
    PerfectForward(std::move(b)); 
    // 1、const 左值引用
    // 2、const 右值引用
    return 0;
}
​

5.2 - 使用场景

C++11 中,STL 容器的插入接口添加了右值引用的版本,以 list 容器的 push_back 接口为例

void push_back(const value_type& val);
void push_back(value_type&& val);

list.h

#pragma once
​
#include <utility>
​
namespace yzz
{
    template<class T>
    struct list_node
    {
        list_node<T>* _prev;
        list_node<T>* _next;
        T _data;
​
        list_node(const T& data = T())
            : _prev(nullptr), _next(nullptr), _data(data)
        { }
​
        // 使用完美转发调用移动构造函数
        list_node(T&& rr)
            : _prev(nullptr), _next(nullptr), _data(std::forward<T>(rr))
        { }
    };
​
    template<class T>
    class list
    {
        typedef list_node<T> Node;
​
        void empty_initialize()
        {
            _phead = new Node;
            _phead->_prev = _phead;
            _phead->_next = _phead;
        }
    public:
        list() { empty_initialize(); }
​
        void push_back(const T& data)
        {
            Node* newnode = new Node(data);
            newnode->_prev = _phead->_prev;
            newnode->_next = _phead;
​
            _phead->_prev->_next = newnode;
            _phead->_prev = newnode;
        }
​
        void push_back(T&& rr)
        {
            // 使用完美转发调用移动构造函数
            Node* newnode = new Node(std::forward<T>(rr));  
            newnode->_prev = _phead->_prev;
            newnode->_next = _phead;
​
            _phead->_prev->_next = newnode;
            _phead->_prev = newnode;
        }
    private:
        Node* _phead;
    };
}

test.cpp

#include "string.h"
#include "list.h"
​
int main()
{
    yzz::list<yzz::string> lt;
    // string(const string& s)
    cout << "-----------------------------" << endl;
    yzz::string s1("11111");
    lt.push_back(s1);
    // string(const string& s)
    cout << "-----------------------------" << endl;
    lt.push_back("22222");
    // string(string&& rr)
    return 0;
}

图解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1167084.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

宽以光电厚以激光 组合式测宽测厚仪 性价比更高

对于板材类产品而言&#xff0c;对其几何尺寸的品质检测&#xff0c;往往是宽度与厚度兼具的&#xff0c;为了同时满足两种检测需求&#xff0c;制造了组合式测宽测厚仪。 单独的测宽仪的原理有光电、机器视觉、激光等&#xff0c;光电测宽仪不仅可选择的测量原理多&#xff0c…

若依分离版——配置多数据源(mysql和oracle),实现一个方法操作多个数据源

目录 一、若依平台配置 二、编写oracle数据库访问的各类文件 三. 一个方法操作多个数据源 一、若依平台配置 1、在ruoyi-admin的pom.xml添加依赖 <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version…

tensorflow-gpu 找不到指定模块

排除&#xff1a; 1.python编译器是64位 查询教程 2. cuda cudnn版本 均是12.2 可以向下兼容 cmd&#xff1a; nvcc -V即可 另一种方法 tensorflow官网教程 pip install tensorflow_gpu1.12.0 4.安装torch-gpu 检查所在环境 解决&#xff01;&#xff01; conda install …

瑞禧生物分享~今天是 碲化银粉体 Ag2Te CAS:12002-99-2

碲化银粉体 Ag2Te CAS&#xff1a;12002-99-2 纯度&#xff1a;99% 仅用于科研 储藏条件&#xff1a;冷藏-20℃ 简介&#xff1a;碲化银是一种无机化合物&#xff0c;化学式是Ag2Te。它是一种单斜晶体&#xff0c;并以矿物的形式存在于自然界中。化学计量的碲化银具有n型半导…

生成第一个 Blazor 应用

前言&#xff1a;博主文章仅用于学习、研究和交流目的&#xff0c;不足和错误之处在所难免&#xff0c;希望大家能够批评指出&#xff0c;博主核实后马上更改。 概述&#xff1a;Blazor 是一个使用 Blazor 生成交互式客户端 Web UI 的框架使用 C# 创建丰富的交互式 UI。共享使…

rabbitmq的confirm模式获取correlationData为null解决办法

回调函数confirm中的correlationDatanull // 实现confirm回调,发送到和没发送到exchange,都触发 Override public void confirm(CorrelationData correlationData, boolean ack, String cause) {// 参数说明:// correlationData: 相关数据,可以在发送消息时,进行设置该参数// …

重大喜讯 | UMS攸信技术斩获厦门5G应用大赛三等奖!

近日&#xff0c;第三届厦门5G应用大赛获奖项目名单公示&#xff0c;攸信技术的“AI5G柔性生产缺陷检测示范线”项目脱颖而出&#xff0c;荣获「第三届厦门5G应用大赛三等奖」&#xff01; 第三届厦门5G应用大赛获奖项目名单 本次获奖既是对攸信技术5G技术创新的鼓励&#xff0…

Ribbon负载均衡原理

一、先看下流程图 备注&#xff1a;红色后面都为拦截器的逻辑&#xff0c;主要是加载配置文件【LoadBalancerAutoConfiguration】&#xff0c;对发送http请求的RestTemplate进行包装拦截&#xff0c;逻辑拦在拦截器里面。 二、LoadBalancerAutoConfiguration 负载均衡用到配置…

Spring集成高性能队列Disruptor

Disruptor简介 Disruptor&#xff08;中文翻译为“破坏者”或“颠覆者”&#xff09;是一种高性能、低延迟的并发编程框架&#xff0c;最初由LMAX Exchange开发。它的主要目标是解决在金融交易系统等需要高吞吐量和低延迟的应用中的并发问题。 Disruptor特点 无锁并发&#x…

kibana中文设置

安装目录下&#xff1a; 修改config/kibana.yml文件&#xff0c;添加如下配置 i18n.locale: "zh-CN"如图&#xff1a; 保存后&#xff0c;重启kibana即可

分组select获取每组用户最新时间的那条数据的查询方式

分组select获取每组用户最新时间的那条数据的查询方式,使用子查询内连接的方式实现 selecttp.user_id AS userId,tp.operator,DATE_FORMAT(tp.create_time,%Y-%m-%d %H:%i:%s) AS operateTimefrom 表名 t1 INNER JOIN(select user_id, max(create_time) max_time from 表名whe…

LeakCanary - Android的内存泄漏检测库

官网 GitHub - square/leakcanary: A memory leak detection library for Android. LeakCanary 项目简介 LeakCanary’s knowledge of the internals of the Android Framework gives it a unique ability to narrow down the cause of each leak, helping developers drama…

知识付费系统小程序开发中的最新趋势和技术是什么?

在迅速发展的移动应用市场中&#xff0c;知识付费系统小程序成为了在线学习和知识传递的重要形式。随着技术的不断进步&#xff0c;了解最新的趋势和技术对于开发知识付费系统小程序至关重要。本文将讨论当前在这一领域中备受关注的最新趋势和技术。 1. 跨平台开发框架的崛起…

赢在电商设计!2024年最新电商设计实战技巧盘点

双十一、双十二、黑五的电商大促即将轮番将至&#xff0c;电商运营人迎来大忙季&#xff0c;选品、直播、采购入库、售后……各种环节都是影响电商转化的关键因素&#xff0c;而电商设计作为打通这些环节&#xff0c;打造高转化率电商平台的关键要素&#xff0c;能够吸引用户注…

淘宝(PC端和APP端)商品详情API接口,可批量采集,高并发请求

淘宝&#xff08;PC端和APP端&#xff09;商品详情API接口&#xff0c;可批量采集&#xff0c;高并发请求代码如下&#xff1a; 淘宝商品详情API接口可以获取到商品的详细信息&#xff0c;以下是一个示例&#xff1a; 请求方式&#xff1a;使用HTTP或HTTPS协议&#xff0c;向指…

android studio安卓模拟器启动了但是看不到画面解决办法

当你使用android studio开发软件的时候&#xff0c;可能会遇到这个问题&#xff0c;就是当你点击下拉框中的启动模拟器的时候&#xff0c;看着程序是启动了&#xff0c;并且运行了&#xff0c;但是看不到模拟器界面&#xff0c;如下图&#xff1a; 这个时候&#xff0c;就要停止…

老年性聋不仅是听不到那么简单,如何避免让它来的更晚一些?

你是否有过这样的经历&#xff0c;和老年人聊天时&#xff0c;他们总是让你重复说话&#xff0c;或者说“你说话太小声了&#xff0c;我听不清楚”&#xff1f;这可能是老年性聋的一个信号。 据统计&#xff0c;老年性聋是全球老年人口中第二大常见疾病&#xff0c;也是全球第三…

【React】【react-globe.gl】3D Objects效果

目录 想要实现的效果实现过程踩坑安装依赖引入页面 想要实现的效果 示例地址 实现过程 踩坑 示例是通过script引入的依赖&#xff0c;但本人需要在react项目中实现该效果。按照react-globe.gl官方方法引入总是报错 Cant import the named export AmbientLight from non EcmaS…

pb:导入EXCEL,提示“不能连接EXCEL”

pb:导入EXCEL,提示“不能连接EXCEL” ------------------------------------------------------------------------------------------------------------------------------- 1.pb连上EXCEL代码: //从EXCEL读取文件 STRING LS_PATH,LS_FILE,ls_file_tmp oleobject ole_1…

三国志14信息查询小程序(历史武将信息一览)制作更新过程02-基本架构

0&#xff0c;前期准备 &#xff08;1&#xff09;一台有公网IP的云服务器&#xff0c;服务器上安装MySQL数据库&#xff0c;启用IIS服务。出入端口号配置运行&#xff08;服务器和平台都要配置&#xff09;&#xff0c;IIS服务器上安装SSL证书 &#xff08;2&#xff09;域名…