[C++11#45](二) 右值引用 | 移动语义 | 万能引用 | 完美转发forward | 初识lambda

news/2025/1/15 18:35:27/

目录

一. 右值引用

1.左值 vs 右值

2.左值引用 vs 右值引用

右值引用实现的两种底层优化

Q1: 容器上

Q2: 字符串上

解决:右值引用

3.完美转发

完美转发

4.补充

1.移动赋值

2.右值引用引用左值的场景

二.lambda

1.引入

2.lambda表达式语法


一. 右值引用

1.左值 vs 右值

什么是左值?

  • 左值是一个表示数据的表达式
  • 如:变量名、解引用的指针
  • 左值:可以取地址 + 对它赋值

什么是右值?

  • 右值是一个表示数据的表达式
  • 如:字面常量、表达式返回值、传值返回函数的返回值
  • 右值:不能取地址
  • 内置类型右值 – 纯右值
  • 自定义类型右值 – 将亡值
    //右值// cout << &10 << endl;// cout << &(x+y)<< endl;// cout << &(fmin(x, y)) << endl;cout << &("xxxxx") << endl;//传的是首元素地址,所以是左值cout << &p[2] << endl;//常量字符串比较特殊,虽然不能修改,但还是左值

2.左值引用 vs 右值引用

  • 引用是取别名
  • 左值引用:给左值取别名
  • 右值引用:给右值取别名

左值引用能否给右值取别名?

  • const左值引用==右值

右值引用能否给左值取别名?

  • 右值引用可以引用move以后的左值
int main()
{double x = 1.1, y = 2.2;// 左值引用:给左值取别名int a = 0;int& r1 = a;// const左值引用可以const int& r2 = 10;const double& r3 = x + y;//通不过,因为返回值的临时变量是右值,要加const// 右值引用:给右值取别名int&& r5 = 10;double&& r6 = x + y;// 右值引用可以引用move以后的左值int&& r7 = move(a);return 0;
}

注意:

  • 左值可以取地址,哪怕 const 不可变动了,能取出地址就是左值

左值引用的使用场景和价值是什么?

使用场景:做参数和做返回值都可以提高效率 价值->减少拷贝

传右值调用下面哪个函数?

构成函数重载,编译器会去找更匹配的

//是否构成函数重载 -- 是
void func(const int& r)
{cout << "void func(const int& r)" << endl;
}void func(int&& r)//更匹配
{cout << "void func(int&& r)" << endl;
}

右值引用实现的两种底层优化

右值对左值短板的解决

思考

左值引用的意义

首先,我们需要理解左值引用的意义。左值引用主要用于以下场景:

  • 函数传参:通过引用传递参数可以避免不必要的拷贝,提高效率。
  • 函数返回值通过引用返回可以避免创建临时对象的拷贝。(但返回对象要是全局的)
    左值引用的确解决了函数传参的问题,无论是左值还是右值都可以通过左值引用传递。然而,左值引用并没有完全解决以下问题:

对于返回局部变量的引用:当函数返回一个局部变量的引用时,由于局部变量在函数作用域结束后将被销毁,返回这样的引用将导致悬垂引用。
以下是一个使用左值引用的示例:

template<class T>
void func1(const T& x) {// 使用引用传递参数,避免拷贝
}
template<class T>
const T& func2(const T& x) {// ...return x; // 返回左值引用
}
int main() {vector<int> v(10, 0);func1(v); // 传递左值func1(vector<int>(10, 0)); // 传递右值return 0;
}

在上面的 func2 中,如果 x 是一个局部变量,那么返回它的引用是不安全的,因为 x 在函数返回后就会被销毁。

右值引用的价值

可以解决以下问题:

  • 避免不必要的拷贝:特别是在函数返回值时,右值引用允许我们以移动语义的方式返回对象,从而避免了深拷贝。
    在 C++11 之前,为了避免深拷贝,我们通常采用输出型参数:
Q1: 容器上

传值返回铁铁的是一个深拷贝,如果是一个int,string都还好说,但是如下面的杨辉三角传值返回一个vector的vector就很麻烦了

vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;
}

下面这种方法避免了深拷贝,但它要求调用者必须提前准备好一个空的 vector 并传递给它,这在某些情况下可能不太直观。

// 解决方案:换成输出型参数,就没了深了拷贝
// 但用起来很别扭
void generate(int numRows, vector<vector<int>>& vv) {vv.resize(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}
}
Q2: 字符串上

通过调用自己模拟实现的 string.h 来理解 C++11 底层的优化,对于函数返回值的接收,要进行 3 次深拷贝

{bit::string str("xxxxxxxxxxxxxxx");cout<<str<<endl;return str;
}
int main()
{bit::string ret2=func();


解决:右值引用

返回值的函数,编译优化后,底层默认右值引用去掉了跨函数的中间商

编译器对右值引用的两个场景:

  • 连续的构造/拷贝构造,合二为一 ———移动拷贝
  • 编译器把 str 识别成右值--将亡值(特殊处理)

强行识别成右值的优化,少去了临时变量的接收,简直是天才,性能 upup

  • 左值& 就像是变量一类的,

(优化前的短板)对于局部变量做函数返回值采取深拷贝,复制一份

  • 右值&& 相当于是 const 化了,有像常量这种纯右值
  • 右值还有将亡值(return 开辟临时空间存储的),采取移动构造,降低拷贝的代价,常见情景

C++在乎性能的升华,Java 就不太 care 了

总结一下:
右值引用和左值引用减少拷贝原理和应用场景不太一样

  • 左值引用是取别名,直接起作用
  • 右值引用是间接起作用,实现移动构造和移动赋值,在拷贝的场景中,如果是右值(将亡值),转移资源

库里面的容器都是深拷贝,因此都实现了移动构造和移动赋值,进行了底层的优

右值就是对于将死值,在销毁之前就实现夺舍了,优化了临时拷贝的构建

  • 右值引用的价值之一:补齐最后一块短板,传值返回的拷贝问题。编译器优化,如上
    右值引用的价值之二:对于插入一些右值数据,也可以减少拷贝。move一下,如下
  • 场景 1(字符串): 右值的夺舍观察:move 返回值是右值,可以被夺舍,所以 copy2 不可以,3 可以

  • 场景 2(容器):容器的插入接口,如果插入对象是右值,可以利用移动构造转移资源给数据结构中的对象。

以 list 的 push_back 自己构造的和 C++11 的对比为例,对字符串(左值)


3.完美转发

1.万能引用

对于T&&的模板传参

万能引用:既可以接收左值,又可以接收右值,利用了模板的识别

  • 实参左值,他就是左值引用(引用折叠)
  • 实参右值,他就是右值引用
template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}int main()
{PerfectForward(10);           // 右值int a;PerfectForward(a);            // 左值 //折叠后相当于是 T&=int的引用PerfectForward(std::move(a)); // 右值 const int b = 8;PerfectForward(b);		      // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

测试:

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; }// 万能引用:既可以接收左值,又可以接收右值

接收的参数识别到的是左值,除了临时返回值的优化,在函数内的函数调用 都是将右值引用的结果看为左值的

Fun(move(t));

引用的底层是什么?指针

理解 int&& r=10,r 接收右值的别名但还是左值,r 可以取地址,又开了一块空间,是要可以修改的

int b=a;
int& b=a;
int b=10;
int&& b=10;

注意点:

右值引入的初心是为了优化返回的深拷贝,对于内部传参还是视为左值,那如果有时想保持右值属性呢

2.完美转发

保持属性

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

对之前自己模拟的 STL,进行函数重载识别右值后,进行移动拷贝的优化:

往下一层传,希望保持属性:右值引用+完美转发

不能去掉,必须重载,因为万能引用的 T 不能是自己实例化的

如果一定不想重载,调用万能引用,就要这样写

⭕编译器优化的发展思路:

  • 左值引用,实现构造传类
  • 将死值返回,移动构造,引出右值引用
  • 右值被识别为左值,如果有时要进行保持呢?右值引用+完美转发(+万能应用//合二为一)

4.补充

1.移动赋值

不仅仅有移动构造,还有移动赋值:

to_string 函数

to_string 函数将一个整数值转换为字符串。它处理正数和负数两种情况,并且在转换过程中反转字符串以确保数字的正确顺序。

string to_string(int value)
{bool flag = true;if (value < 0){flag = false;value = 0 - value; // 将负数转为正数}string str;while (value > 0){int x = value % 10; // 取个位数value /= 10; // 除以10,去掉个位数str += ('0' + x); // 将数字转换为字符并添加到字符串末尾}if (flag == false){str += '-'; // 添加负号}reverse(str.begin(), str.end()); // 反转字符串,使数字顺序正确return str;
}
  • operator= 移动赋值

string& operator=(string&& s) 是一个移动赋值操作符,它用于将一个右值引用的 string 对象赋值给当前的 string 对象。这里的右值引用 string&& s 表明 s 是一个临时对象或者是将亡值。

string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值(资源移动)" << endl;swap(s); // 使用swap交换当前对象和传入对象的资源return *this; // 返回当前对象的引用
}

这里使用了 swap 方法来交换两个对象的资源,而不是复制或重新分配内存。

  • main 函数

main 函数中,创建了一个 string 对象 ret1,然后调用 to_string 函数将整数 1234 转换成字符串,并将结果赋值给 ret1

int main()
{string ret1;ret1 = to_string(1234);return 0;
}
  • 运行结果

当运行这段代码时,输出如下:

string& operator=(string&& s) -- 移动赋值(资源移动)

这是因为在 main 函数中,to_string(1234) 返回一个临时的 string 对象,这个临时对象被移动赋值给了 ret1。因此,触发了 string& operator=(string&& s) 的调用,输出了移动赋值的信息。

2.右值引用引用左值的场景

右值引用引用左值及其一些更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?

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

典例:对字符串左值) 进行右值引用

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) -- 移动语义

二.lambda

优化函数指针

1.引入

对于类,用仿函数进行比较

struct Goods
{string _name;  // 名字double _price; // 价格int _evaluate; // 评价// ...Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };// 评价、价格sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());return 0;
}

2.lambda表达式语法

格式:[capture-list] (parameters) mutable -> return-type { statement }

lambda表达式各部分说明

  1. [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  2. (parameters):参数。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  3. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  4. ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  5. {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

下面这样写个lambda表达式,就很清晰

int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };//sort传一个可调用的匿名对象sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {return gl._price < gr._price;//价格升序});sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {return gl._price > gl._price; });//价格降序return 0;
}

注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

对于 lambda 的详细使用,下篇文章见~


http://www.ppmy.cn/news/1521964.html

相关文章

无人机之载重篇

无人机的载重能力是一个复杂且多样化的参数&#xff0c;它受到多种因素的影响&#xff0c;包括无人机的类型、设计、技术规格以及用途等。以下是对无人机载重能力的详细解析&#xff1a; 一、无人机载重能力的差异 无人机的载重能力差异很大&#xff0c;从几百克到几十千克不等…

Java后端分布式系统的服务容错机制:Faul-tolerant Systems

Java后端分布式系统的服务容错机制&#xff1a;Faul-tolerant Systems 大家好&#xff0c;我是微赚淘客返利系统3.0的小编&#xff0c;是个冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在构建分布式系统时&#xff0c;服务的容错性是确保系统稳定性和可用性的…

安宝特科技 | AR眼镜在安保与安防领域的创新应用及前景

随着科技的不断进步&#xff0c;增强现实&#xff08;AR&#xff09;技术逐渐在多个领域展现出其独特的优势&#xff0c;尤其是在安保和安防方面。AR眼镜凭借其先进的功能&#xff0c;在机场、车站、海关、港口、工厂、园区、消防局和警察局等行业中为安保人员提供了更为高效、…

TCP 和 UDP 区别

UDP UDP&#xff08;用户数据报协议&#xff0c;User Datagram Protocol&#xff09;是一种无连接的网络传输协议&#xff0c;提供了简单的消息传送服务。UDP位于传输层&#xff0c;允许应用程序向其他主机发送封装在IP数据报中的消息&#xff0c;而无需先建立连接。由于UDP不…

【论文笔记】Multi-Task Learning as a Bargaining Game

Abstract 本文将多任务学习中的梯度组合步骤视为一种讨价还价式博弈(bargaining game)&#xff0c;通过游戏&#xff0c;各个任务协商出共识梯度更新方向。 在一定条件下&#xff0c;这种问题具有唯一解(Nash Bargaining Solution)&#xff0c;可以作为多任务学习中的一种原则…

I2VGen-XL模型构建指南

一、介绍 VGen可以根据输入的文本、图像、指定的运动、指定的主体&#xff0c;甚至人类提供的反馈信号生成高质量的视频。它还提供了各类常用的视频生成模型工具&#xff0c;例如可视化、采样、训练、推理、使用图像和视频的联合训练&#xff0c;加速等各类工具和技术。 &quo…

微信小程序显示后台文章副文本,图片和视频正常显示

解决方案: 使用 wxParse 或 rich-text 组件: 这两种方式可以解析 HTML 字符串并渲染富文本内容&#xff0c;包括图片和视频。 数据处理: 将后台返回的富文本数据进行处理&#xff0c;提取出图片和视频的链接&#xff0c;并将其转换成小程序支持的格式。 方案一&#xff1a;使…

数据库学习01——mysql怎么创建数据库和表

第一步&#xff1a;创建数据库 使用 create database 语句&#xff0c;后跟要创建的数据库名称&#xff1a; CREATE DATABASE dbname;例如&#xff0c;要创建名为 my_db 的数据库&#xff0c;请输入&#xff1a; CREATE DATABASE my_db ;使用 show databases; 语句检查数据库是…