【C++】右值引用与移动语义详解:如何利用万能引用实现完美转发

C++语法 相关知识点 可以通过点击 以下链接进行学习 一起加油!
命名空间 缺省参数与函数重载 C++相关特性 类和对象-上篇 类和对象-中篇
类和对象-下篇 日期类 C/C++内存管理 模板初阶 String使用
String模拟实现 Vector使用及其模拟实现 List使用及其模拟实现 容器适配器Stack与Queue Priority Queue与仿函数
模板进阶-模板特化 面向对象三大特性-继承机制 面向对象三大特性-多态机制 STL 树形结构容器 二叉搜索树
AVL树 红黑树 红黑树封装map/set 哈希-开篇 闭散列-模拟实现哈希
哈希桶-模拟实现哈希 哈希表封装 unordered_map 和 unordered_set C++11 新特性:序章

大家好,我是店小二。本篇文章将深入讲解C++11的新增特性,重点围绕右值引用与移动语义展开,并详细讨论如何利用万能引用实现完美转发。如果在阅读过程中有疑问或不同的见解,欢迎随时私信我进行进一步交流


🌈个人主页:是店小二呀

🌈C语言专栏:C语言

🌈C++专栏: C++

🌈初阶数据结构专栏: 初阶数据结构

🌈高阶数据结构专栏: 高阶数据结构

🌈Linux专栏: Linux

🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅

文章目录

一、左值和左值引用&

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获得它的地址,也可以对它就行赋值操作。左值可以出现在赋值符号的左边的,右值不能出现在赋值符号左边。定义时const修饰后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

cpp 复制代码
int main()
{
	//左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	//左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;

	return 0;
}

二、右值和右值引用&&

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名 。

cpp 复制代码
string to_string(int val)
{
	string str;
	//...
	return str;
}

int main()
{
    double x = 1.1, y = 2.2;
    //以下都是常见的右值
    10;
    x + y;

    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    
    //匿名对象的声明周期只有这一行
	string&& rref3 = string("1111");
	string&& rref4 = to_string(123);
    // 这里编译会报错:error C2106: "=": 左操作数必须为左值
    10 = 1;
    x + y = 1;
  
    return 0;
}

右值分类

  • 纯右值:内置类型(不需要多关注)
  • 将亡值:自定义类型(匿名对象,临时对象)
cpp 复制代码
int main()
{
    double x = 1.1, y = 2.2;
    int&& rr1 = 10;
    const double&& rr2 = x + y;
    rr1 = 20;
    rr2 = 5.5; // 报错
    return 0;
}

虽然右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1右值引用对象取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

三、如何判断左值和右值

能只通过是否能被修改去判断左、右值,它们之间的区别在于左值和右值是否能取地址 ,右值是临时性的,其内容通常不可修改,只能通过移动语义(move)将其资源转移到其他对象。虽然右值引用(右值引用变量)虽然可以绑定到右值,但是它本身并不能直接修改右值的内容。

  • 左值:可以取地址 也可能是一个表达式

  • 右值:不可以取地址的

四、左值引用与右值引用比较

左值引用总结:

  • 通常左值引用只能引用左值,不能引用右值
  • const左值引用即可引用左值,也可以引用右值(临时性)
cpp 复制代码
int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

右值引用总结:

  • 右值引用只能右值,不能引用左值
  • 但是右值引用可以move以后的左值
cpp 复制代码
int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	// error C2440: "初始化": 无法从"int"转换为"int &&"
	// message : 无法将左值绑定到右值引用
	int a = 10;
	int&& r2 = a;
	// 右值引用可以引用move以后的左
	int&& r3 = std::move(a);
	return 0;
}

五、右值引用与移动语义(重点)

5.1 彻底解决传值返回拷贝问题

左值引用的场景:

cpp 复制代码
void func1(const string& s);
string& func2();

左值引用做返回值的问题没有彻底解决,如果返回值是func2中局部对象,不能用引用返回,只能通过传值返回,那么传值返回会导致至少1次拷贝构造(如果是一些老一点的编译器可能是两次拷贝构造,没有进行优化)

问题:既然左值引用做返回值不行,那么这里是将右值引用,使用右值引用做返回值是否能解决该问题?

首先无论是左值引用还是右值引用,面对局部变量出了作用域就会销毁,无法进行干预。对此右值引用不能解决局部变量的问题,是用于解决局部变量所掌握资源的生命周期的问题。

5.2 右值引用和移动语义延长局部变量掌握资源生命周期

在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

cpp 复制代码
//右值(将亡值)
// 移动构造
string(string&& s)
    :_str(nullptr)
        ,_size(0)
        ,_capacity(0)
    {
        cout << "string(string&& s) -- 移动语义" << endl;
        swap(s);
    }

首先我们先理解下这段代码的逻辑,这里参数部分专门用来接收右值(将亡值),但是右值引用属性是左值(这一点,等下谈论),将亡值意味着生命周期十分短暂,这样意味着将亡值所掌握的资源也会销毁,这样子就会导致十分浪费 ,对此直接 swap(s);夺舍他的资源,反正你的生命不多了,不如将资源交给我,所以它叫做移动构造,就是窃取别人的资源来构造自己。 (以下临时变量没有画出来,但是移动构造目的如此),这样可以解决面对传值返回导致深拷贝,减低效率的问题。这里不需要深拷贝,只需要交换下资源就行了

这里移动构造没有解决或延长临时变量生命周期,而是延长了资源的生命周期。

5.3 移动构造隐含代价

cppp 复制代码
int main()
{
	std::string s1("1111111111111");
	std:string s2 = s1;
	std::string s3 = move(s1);
	return 0;
}

由于移动构造,就是窃取别人的资源来构造自己。那么当发生移动构造,被窃取的那一方就会被盗取(资源属于别人的)。所以在使用移动构造时,需要保证被窃取对象是不健康(不需要的),所以s1不要随便变成右值属性。自己的器官给了就是自己没有了

5.4 移动赋值

在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

cpp 复制代码
// 移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    swap(s);
    return *this;
}
int main()
{
    bit::string ret1;
    ret1 = bit::to_string(1234);
    return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值

5.5 编译器的优化

  • 分析图一,这里str属性为左值,在传值返回时进行拷贝构造生成临时变量,临时变量具有常量属于为右值,通过右值进行初始化走移动构造
  • 分析图二,这里str属性为左值,如果不行move话,需要走一次拷贝构造那么销毁较大,编译器优化将str返回时,将str属性转化为右值属性,进行移动构造,由于接下需要再移动构造,编译器优化后,合二为一只需要一次移动构造即可,这里把str处理成右值,相当于move一下。

5.5.1 同性质函数进行优化

这里无论是拷贝构造+赋值拷贝,还是移动构造+移动赋值,编译器都无法优化。因为这里两个不是同一个性质的函数,编译器不好合二为一,没有强行参与。这里重点还是分析图二右边的图,帮助我们更好地深入了解移动语义。

具体分析过程:

  • 首先这里有临时对象进行移动构造,首先临时对象先跟str交换下,临时对象指向str原本指向的资源,str指向临时对象原本指向的空间(空),之后str生命周期结束,在被ret1接收时,又进行了一次移动语义,ret一开始指向了一块资源空间,临时对象是将亡值,把这块指向空间带走,帮我释放空间,重演了一遍刚刚的操作,真是一举两得。这里原本要释放三个资源,两次拷贝演变成只需要释放一个资源没有拷贝资源,效率得到了大幅度的提升。

六、现代写法和移动语义的区别

C++11 中,swap 的现代写法和移动语义有一些关联,都是进行交换,而现代写法是窃取已经完成拷贝的资源,效率上没有提高,形式上简洁了许多,而移动语义是直接窃取资源,大幅度提高了效率。

七、右值引用本身属性

从图可以得到,右值引用本身就是左值,只有右值引用本身处理成左值,这样的才能实现移动移动构造和移动赋值,转移资源语法是自洽的。

右值引用的属性如果是右值,那么移动构造和移动赋值,要转移资源的语法逻辑是矛盾的,右值是不能被改变的,那么swap需要修改是行不通的(可以理解为右值带有const属性)

八、move函数

当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

cpp 复制代码
template<class _Ty>
    inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
    {
        // forward _Arg as movable
        return ((typename remove_reference<_Ty>::type&&)_Arg);
    }

九、STL容器插入接口函数也增加了右值引用版本

直通list

直通vector

cpp 复制代码
void push_back (value_type&& val);
int main()
{
    list<bit::string> lt;
    bit::string s1("1111");
    // 这里调用的是拷贝构造
    lt.push_back(s1);
    // 下面调用都是移动构造
    lt.push_back("2222");
    lt.push_back(std::move(s1));
    return 0;
}
运行结果:
    // string(const string& s) -- 深拷贝
    // string(string&& s) -- 移动语义
    // string(string&& s) -- 移动语义

push_back 就是涉及到拷贝的目标,这里匿名对象就行。可以看出移动构造几乎没有代价,移动资源就行。

十、右值引用导致属性改变

这里调用insert进行复用,但是这里x是右值引用接收,由于属性变为左值属性,这里需要对insert进行函数重载,每一层都要有一个右值引用的版本。如果使用万能引用就可以大程度减少这份部分工作。

如果每一层都需要考虑属性发生变化,当复用接口的过程中,想要保持右值属性需要去move一下,会导致十分麻烦和花时间

处理保证是右值(用于属性的改变和move的转化),上面效率提升,针对的是自定义类型的深拷贝的类,因为深拷贝的类才有转移资源的移动系列函数,对于内置类型,和浅拷贝自定义类型,没有移动系列函数。如果是内置类型,没有拷贝构造和移动构造的说法,但是有属性的问题。

十、万能引用及其完美转发

10.1 模板中&&万能引用

cpp 复制代码
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}

int main()
{
    PerfectForward(10); // 右值
    int a;
    PerfectForward(a); // 左值

    PerfectForward(std::move(a)); // 右值

    const int b = 8;

    PerfectForward(b); // const 左值
    PerfectForward(std::move(b)); // const 右值
    return 0;
}

模板中&&不代表右值引用,而是万能引用。万能引用确实使得函数模板能够接受任何类型的参数(左值和右值),根据参数属性自动推导左值引用还是右值引用的能力

引用类型的唯一作用就是限制了接收的类型,万能引用的特性使得函数模板能够更加灵活地处理传递给它的参数。

虽然不需要考虑在每一层写函数重载了,但是从打印结果来看,没有解决后续使用中右值都退化成了左值。

如果我们希望能够在传递中保持它的左值或者右值的属性来解决上述问题。

10.2 std::forward 完美转发

完美转发是指在函数模板中以保持类型不变的方式传递参数,包括参数的值类型(左值还是右值)

std::forward 完美转发在传参的过程中保留对象原生类型属性

cpp 复制代码
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
    Fun(std::forward<T>(t));
}
int main()
{
    PerfectForward(10); // 右值
    int a;
    PerfectForward(a); // 左值
    PerfectForward(std::move(a)); // 右值
    const int b = 8;
    PerfectForward(b); // const 左值
    PerfectForward(std::move(b)); // const 右值
    return 0;
}

10.2.1 是否可以通过强制类型转化达到保持值属性

cpp 复制代码
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
    Fun((T&&) t);
}
int main()
{
    PerfectForward(10); // 右值
    int a;
    PerfectForward(a); // 左值
    PerfectForward(std::move(a)); // 右值
    const int b = 8;
    PerfectForward(b); // const 左值
    PerfectForward(std::move(b)); // const 右值
    return 0;
}

虽然从结果上是一样的,但是强制类型转换 (T&&) t 确实只是将 t 强制转换为 T&& 类型,但它并没有保证将 t 的原始值类别(左值或右值)正确地传递给被调用的函数。这种转换方式可能会导致不正确的参数传递行为,推荐使用std::forward(t)在传参的过程中保持了t的原生类型属性。

10.3 关于完美转发的使用场景

cpp 复制代码
template<class T>
    struct ListNode
    {
        ListNode* _next = nullptr;
        ListNode* _prev = nullptr;
        T _data;
    };
template<class T>
    class List
    {
        typedef ListNode<T> Node;
        public:
        List()
        {
            _head = new Node;
            _head->_next = _head;
            _head->_prev = _head;
        }
        void PushBack(T&& x)
        {
            //Insert(_head, x);
            Insert(_head, std::forward<T>(x));
        }
        void PushFront(T&& x)
        {
            //Insert(_head->_next, x);
            Insert(_head->_next, std::forward<T>(x));
        }
        void Insert(Node* pos, T&& x)
        {
            Node* prev = pos->_prev;
            Node* newnode = new Node;
            newnode->_data = std::forward<T>(x); // 关键位置
            // prev newnode pos
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = pos;
            pos->_prev = newnode;
        }
        void Insert(Node* pos, const T& x)
        {
            Node* prev = pos->_prev;
            Node* newnode = new Node;
            newnode->_data = x; // 关键位置
            // prev newnode pos
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = pos;
            pos->_prev = newnode;
        }
        private:
        Node* _head;
    };

在代码中使用万能引用(也称为转发引用,T&&)和std::forward可以提高代码的效率,尤其是当你在函数内部需要处理既可以是左值,也可以是右值的场景时。通过使用万能引用结合完美转发 (std::forward),你可以在保留对象原本的值类别(左值或右值)的同时,将其传递给其它函数,避免不必要的拷贝操作,从而提升性能。


以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!

相关推荐
C++忠实粉丝10 分钟前
计算机网络socket编程(2)_UDP网络编程实现网络字典
linux·网络·c++·网络协议·计算机网络·udp
Mongxin_Chan35 分钟前
【Cpp】指针与引用
c++·算法
尽兴-1 小时前
Redis模拟延时队列 实现日程提醒
java·redis·java-rocketmq·mq
SSL_lwz1 小时前
P11290 【MX-S6-T2】「KDOI-11」飞船
c++·学习·算法·动态规划
熬夜学编程的小王1 小时前
【C++篇】从基础到进阶:全面掌握C++ List容器的使用
开发语言·c++·list·双向链表·迭代器失效
悄悄敲敲敲1 小时前
C++:智能指针
开发语言·c++
zhangpz_1 小时前
c ++零基础可视化——vector
c++·算法
萨达大1 小时前
23种设计模式-模板方法(Template Method)设计模式
java·c++·设计模式·软考·模板方法模式·软件设计师·行为型设计模式
刀鋒偏冷2 小时前
ninja: error: ‘/opt/homebrew/Cellar/opensslxxx/xx/lib/libssl.dylib
c++
理论最高的吻2 小时前
98. 验证二叉搜索树【 力扣(LeetCode) 】
数据结构·c++·算法·leetcode·职场和发展·二叉树·c