目录
一. 右值引用
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表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {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 的详细使用,下篇文章见~