〇、前言
本文会讨论C++中的左值,右值,左值引用,右值引用,以及会理清它们之间的关系。
一、左值与右值
(一)概述
-
左值是一般指表达式结束后依然存在的持久化对象。右值指表达式结束时就不再存在的临时对象。便捷的判断方法:能对表达式取地址、有名字的对象为左值。反之,不能取地址、匿名的对象为右值。
-
C++ 表达式(运算符带上其操作数、字面量、变量名等)有两种独立的属性:类型 和值类别 。类型指变量声明时的类型,而值类别是表达式结果的类型,它必属于左值、纯右值或将亡值三者之一 。如
int&& x;
其中 x 的类型为右值引用 ,但作为表达式使用时其值类别为左值(因为有名字,可以取址)。比如:
cpp
#include <iostream>
void func(int &&b) {
std::cout << "b: " << b << " addr: " << &b << std::endl;
b++;
std::cout << b << std::endl;
}
int main() {
int c = 10;
func(std::move(c));
std::cout << "c: " << c << " addr: " << &c << std::endl;
return 0;
}
上例中的 b 就是一个右值引用,它接受了一个右值 c 。在 main()
中,c 被转化为右值引用 传入 func()
,因此,func()
中的 b 其实就是 main()
中的 c 别名,它们指向的是同一块内存区域。需要注意的是,func()
中的 b 其实是一个左值,换句话说,右值引用如果绑定了一个右值,它会延长这个右值的生命周期。 这种生命周期的延长意味着,尽管原始表达式产生的值是一个右值,一旦它被一个右值引用所绑定,它就不再是一个"即将销毁的临时值",而更像是一个普通的变量。 这允许开发者在保证效率的同时,也能够更灵活地控制这些值。
运行结果:
bash
g++ 1.cxx -o main -std=c++14
./main
b: 10 addr: 0x16ba16e28
11
c: 11 addr: 0x16ba16e28
我们需要注意的是,main()
中的 c 被转换成了右值引用,但是它的状态没有发生任何改变。它在 func 中被修改,并在 func 返回后仍然保持有效。std::move
只是告诉编译器一个对象可以被"安全地"当作右值来使用,这样就允许在移动语义上下文中使用该对象。
(二)右值的分类
-
纯右值(prvalue):用于识别临时变量和一些不与对象关联的值。函数返回值为非引用类型、表达式临时值(如1+3)、lambda表达式等。
-
将亡值(xvalue):是与右值引用相关的表达式,通常指将要被移动的对象。如,函数返回类型为
T&&
、std::move
的返回值、转换为T&&
的类型转换函数的返回值(注意,这些都是与右值引用相关的表达式)或临时对象。
表格形式:
类型分类 | 特征 | 绑定规则和类型安全转换 | 实例和特殊情况 |
---|---|---|---|
左值 (lvalue) | 存在变量、函数调用产生的对象,有固定地址。 | 可绑定到左值引用,例如 int& x = a; |
变量 int a = 10; 函数调用(返回引用)例如 int& foo(); |
纯右值 (prvalue) | 不适宜被移动,不具有可识别的地址。 | 可绑定到右值引用或 const 左值引用,例如 int&& x = 5; |
字面量例如 42 ,表达式例如 a + b ,函数调用(返回非引用)例如 int foo(); |
将亡值 (xvalue) | 适宜被移动,具有地址但对象即将被销毁。 | 可绑定到右值引用,例如使用 std::move 例如 int&& x = std::move(a); |
通过 std::move 生成的将亡值,例如 std::move(a) ;使用移动构造函数或移动赋值操作时,传递的实际参数。 |
泛左值 (glvalue) | 概括左值和将亡值,即拥有特定身份(地址)且可能即将被销毁。 | 可以绑定到左值引用或右值引用,取决于上下文。 | 例如使用 decltype 捕获引用时,decltype((a)) x = a; 捕获的是 a 的引用。 |
二、左值引用与右值引用
(一)概述
-
左值引用和右值引用都属于引用类型。无论是声明一个左值引用还是右值引用都必须立即进行初始化(今天考试刚考到)。
-
左值引用都是左值。但具名的右值引用是左值,而匿名的右值引用是右值(比如上面的 func 中的 b,虽然它是右值引用,但是它是一个左值)。
(二)可绑定的值类型(设T是个具体类型)
-
左值引用(T& ):只能绑定到左值(非常量左值)
-
右值引用(T&& ):只能绑定到右值(非常量右值)
-
常量左值引用(
const T&
):常量左值引用是个"万能"的引用类型:它既可以绑定到左值也可以绑定到右值,它像右值引用一样可以延长右值的生命期 。不过相比于右值引用所引用的右值,常量左值引用的右值在它的"余生"中只能是只读的。对于这点,可以参考这个例子:
cpp
#include <iostream>
void func(const int &b) {
std::cout << "b: " << b << " addr: " << &b << std::endl;
b++; // 会报错
std::cout << b << std::endl;
}
int main() {
int c = 10;
func(std::move(c));
std::cout << "c: " << c << " addr: " << &c << std::endl;
return 0;
}
编译出错:
bash
g++ 2.cxx -o main -std=c++14
2.cxx:4:6: error: cannot assign to variable 'b' with const-qualified type 'const int &'
b++;
~^
2.cxx:2:22: note: variable 'b' declared const here
void func(const int &b) {
~~~~~~~~~~~^
1 error generated.
- 常量右值引用(
const T&&
):可绑定到右值或常量右值。由于移动语义需要右值可以被修改,因此常量右值引用没有实际用处。如果需要引用右值且让其不可更改,则常量左值引用就足够了。
三、万能引用(universal reference)
(一)T&&的含义
-
当T是一个具体的类型时,
T&&
表示右值引用,只能绑定到右值。 -
当涉及T类型推导 时,
T&&
为万能引用。若用右值初始化万能引用,则T&&为右值引用。若用左值初始化万能引用,则T&&为左值引用。但不管哪种情况,T&&都是一种引用类型。
(二)万能引用
T&&
是万能引用的两个条件:
(1)必须涉及类型推导;
(2)声明的形式也必须正好形如 T&&
。并且该形式被限定死了,任何对其修饰都将剥夺T&&成为万能引用的资格。
- 万能引用使用的场景
(1)函数模板形参
(2)auto&&
一个例子:
cpp
#include <iostream>
#include <vector>
using namespace std;
class Widget {};
void func1(Widget &¶m){}; // param为右值引用类型(不涉及类型推导)
template <typename T>
void func2(T &¶m) {} // param为万能引用(涉及类型推导)
template <typename T>
void func3(std::vector<T> &¶m) {
}
template <typename T>
void func4(const T &¶m) {}
template <class T> class MyVector {
public:
void push_back(T &&x) {
} // x为右值引用。因为当定义一个MyVector对象后,T己确定。当调用该函数时T的类型不用再推导!
template <class... Args>
void emplace_back(
Args
&&...args){}; // args为万能引用,因为Args独立于T的类型,当调用该函数时,需推导Args的类型。
};
int main() {
Widget w;
func2(w); // 万能引用, func2(T&& param),param为Widget&(左值引用)
func2(std::move(w)); // 万能引用, param为Widget&&,是个右值引用。
int x = 0;
Widget &&var1 = Widget(); // var1为右值引用(不涉及类型推导)
auto &&var2 = var1; //万能引用,auto&&被推导为Widget&(左值引用)
auto &&var3 = x; //万能引用,被推导为int&;(左值引用)
// 3. 计算任意函数的执行时间:auto&&用于lambda表达式形参(C++14)
auto timefunc = [](auto &&func, auto &&...params) {
//计时器启动
//调用func(param...)函数
std::forward<decltype(func)>(
func)( //根据func的左右值特性来调用相应的重载&或&&版本的成员函数
std::forward<decltype(params)>(params)... //保持参数的左/右值特性
);
//计时器停止并记录流逝的时间
};
timefunc(func1, std::move(w)); //计算func1函数的执行时间
return 0;
}
在你提供的代码中,main 函数涉及到万能引用的语句如下:
-
func2(w);
和func2(std::move(w));
--- 这里的func2
函数模板参数T&&
是一个万能引用。它可以绑定到左值和右值。在这两个调用中,第一次调用时T
被推导为Widget&
(因为w
是一个左值),而第二次调用时T
被推导为Widget
(因为std::move(w)
产生一个右值)。 -
auto &&var2 = var1;
--- 这里使用了auto&&
,它也是一个万能引用。var1
是一个左值(尽管它本身是一个绑定到临时对象的右值引用),所以var2
被推导为Widget&
。 -
auto &&var3 = x;
--- 这同样使用了auto&&
,这是一个万能引用。由于x
是一个左值,var3
被推导为int&
。 -
在
timefunc
lambda 表达式的定义中,参数列表(auto &&func, auto &&...params)
使用了万能引用。这里auto&&
用于单个参数和参数包,允许这个 lambda 接受任意数量的任意类型的参数,并保持他们的值类别(左值或右值)。
这些例子展示了万能引用在模板类型推导中的强大功能,尤其是在泛型编程和函数重载解析中的应用。通过万能引用,可以写出更灵活的函数和模板,使得它们能够同时接受左值和右值,而无需重载函数。
四、参考
这里。