【C++语法】C++11——右值引用
C++11——右值引用
文章目录
- C++11——右值引用
- 统一的列表初始化
- auto声明与decltype推导类型
- nullptr
- 一些新增的容器
- array
- forward_list
- 右值引用与移动构造
- 右值引用与左值引用
- 左值引用与右值引用的交叉使用与限制
- 使用场景及其他要点
- 移动构造和移动赋值
- 完美转发
C++的版本更新
统一的列表初始化
C++11引入列表初始化,允许所有类型使用列表初始化并省略赋值符号,这是一个奇怪但支持大一统初始化的特性。C语言仅支持数组和结构体的列表初始化,C++扩展为一切皆可列表初始化。变量、有构造函数的对象都可以用列表初始化。
struct Point
{int _x;int _y;
};
int main()
{int array1[] = {1, 2, 3, 4, 5};int array2[5] = {0};Point p = {1, 2};//int array1[]{ 1, 2, 3, 4, 5 };//int array2[5]{ 0 };//Point p{ 1, 2 };// C++11中列表初始化也可以适用于new表达式中int* pa = new int[4]{ 0 };return 0;
}
auto声明与decltype推导类型
-
auto声明可以用于长类型或不方便写的类型,如指针函数指针,让编译器自动推导类型。
-
decltype可以推导一个类型,与typeid不同,typeid只能获取类型的字符串表示,不能用于声明变量或传递模板参数。decltype推导的类型可以用于定义对象或传递模板参数。decltype可以接受表达式或值作为参数。decltype在某些场景下非常有用,例如当需要推导一个长类型的迭代器类型时,可以使用decltype来简化代码。
-
decltype可以与auto配合使用,推导出类型后可以传递给模板参数或定义其他变量。decltype的单词原意是声明而非推导,但其功能是推导类型并用于声明。但要注意的是:
decltype
是一个类型说明符,它推导出给定表达式或实体(变量、函数等)的类型,然后你可以用这个类型去声明变量或模板参数。它不是“声明”动作,而是提供“类型”。// decltype的一些使用使用场景 template <class T1, class T2> void F(T1 t1, T2 t2) {decltype(t1 * t2) ret;cout << typeid(ret).name() << endl; } int main() {const int x = 1;double y = 2.2;decltype(x * y) ret; // ret的类型是doubledecltype(&x) p; // p的类型是int*cout << typeid(ret).name() << endl;cout << typeid(p).name() << endl;F(1, 'a');return 0; }
nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
一些新增的容器
array
- array是C++11引入的静态数组容器,对原生静态数组进行了封装。array提供了迭代器支持,但不支持动态扩容操作如push_back。
- array的空间在创建时就已固定,只能通过operator[]等方式访问元素。
- array会检查数组越界访问,使用断言或异常机制。
std::array::operator[]
默认不进行边界检查,它的行为和内置数组一样,越界访问是未定义行为。进行边界检查的是std::array::at()
成员函数,它在越界时会抛出std::out_of_range
异常
forward_list
forward_list
是C++中的单向链表实现,其名称中的forward表示"向前"的意思。与普通的list(双向链表)相比,forward list每个节点节省了4字节空间。- 它支持头插和头删操作,但不支持尾插和尾删,因为尾删需要找到前一个节点,代价很高。
insert_after
和erase_after
操作也不是删除当前位置,而是删除当前位置之后的节点,因为要找到前一个位置需要从头开始遍历,效率很低。forward list提供了与list类似的接口版本,但功能上存在一些不支持的操作。它可以像迭代器一样访问,返回底层对应的指针。
右值引用与移动构造
右值引用与左值引用
-
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。(可以取地址才是区分的关键)
int main() {// 以下的p、b、c、*p都是左值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; }
-
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。(不可以取地址才是区分的关键)
-
需要注意的是右值是不能取地址的,但是给右值取别名后(右值引用),会导致右值被存储到特定位置,且可以取到该位置的地址
int main() {double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int &&rr1 = 10;double &&rr2 = x + y;double &&rr3 = fmin(x, y);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0; }
-
从本质上看,右值都是临时性的对象或值,包括常量值、运算结果临时存储对象和函数返回值临时对象。
-
左值引用和右值引用在语法层有区别,但在汇编层都转换为指针,没有引用概念。从汇编层看,引用就是指针,语法层的区别在汇编层不存在。左值引用在语法层是取别名不开空间,但底层开空间。右值引用在语法层是取别名不开空间,但底层开空间。
-
右值实际上是有地址空间的,编译器会为右值表达式开辟临时空间。右值引用能够绑定到右值是因为底层存在这些临时空间。语法层限制直接取右值的地址是为了保证类型安全。右值引用的实现依赖于编译器内部的临时空间分配机制。虽然语法上右值不能取地址,但底层实现必须为所有数据分配存储空间。
左值引用与右值引用的交叉使用与限制
-
左值引用只能引用左值,不能引用右值。但是const左值引用既可引用左值,也可引用右值
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以后的左值。move是标准库中的函数,返回右值引用。
int main() {// 右值引用只能右值,不能引用左值。int &&r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;// int &&r2 = a;// 右值引用可以引用move以后的左值int &&r3 = std::move(a);return 0; }
使用场景及其他要点
-
左值引用:主要用于函数参数传递和返回值两个场景,其核心价值在于减少数据拷贝,避免了深拷贝等性能开销。左值引用的局限性,特别是返回值场景中的问题。但是当返回局部对象时无法使用左值引用,因为局部对象在函数返回后会被销毁,导致引用失效。
-
右值引用本身具有左值属性,这是为了能够修改被引用的右值对象。虽然右值在语法上通常不能修改,但实际在内存中是有存储空间的。右值引用将右值转换为左值后,就可以进行资源转移操作。这种设计使得移动构造能够正常工作,是C++11引入的重要特性。在实现移动构造时,需要将右值引用视为左值才能进行资源交换。如果不这样设计,就无法实现高效的资源转移。
假设我们现在实现一个简单的list,下面是部分代码
//list void push_back(const T& x) {cout << "list: push_back(const T& x)——拷贝构造" << endl;insert(end(), x); }void push_back(T&& x) {cout << "list: push_back(T&& x)——移动构造" << endl;insert(end(), x); //bug?? }iterator insert(iterator pos, const T& x) {cout << "list: iterator insert(iterator pos, const T& x)" << endl;Node* cur = pos._node;Node* newnode = new Node(x);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode); }iterator insert(iterator pos, T&& x) {cout << "list: iterator insert(iterator pos, T&& x)" << endl;Node* cur = pos._node;Node* newnode = new Node(x);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode); }int main() {bit::list<int> lt;//示例一:int n = 1;lt.push_back(n);cout << endl;//示例二:lt.push_back(2);cout << endl;//示例三:lt.push_back(move(n));cout << endl;return 0; }
在这个代码中,main函数会根据参数调用最合适的
push_back
,而push_back
会去调用最合适的insert
。下面是运行结果:list: push_back(const T& x)——拷贝构造 list: iterator insert(iterator pos, const T& x)list: push_back(T&& x)——移动构造 list: iterator insert(iterator pos, const T& x)list: push_back(T&& x)——移动构造 list: iterator insert(iterator pos, const T& x)
可以看到,示例二和示例三调用的
push_back
都是移动构造的重载函数,但是这个函数调用的都是非移动构造的insert
函数,因为在push_back(T&& x)
中参数x
是一个左值而并非右值,所以调用的都是list: iterator insert(iterator pos, const T& x)
,所以说这个push_back(T&& x)
其实是有bug的,最正确的写法是:void push_back(T&& x) {cout << "list: push_back(T&& x)——移动构造" << endl;insert(end(), move(x));//这里要加一个move函数 }
运行结果:
list: push_back(const T& x)——拷贝构造 list: iterator insert(iterator pos, const T& x)list: push_back(T&& x)——移动构造 list: iterator insert(iterator pos, T&& x)list: push_back(T&& x)——移动构造 list: iterator insert(iterator pos, T&& x)
移动构造和移动赋值
在C++中,函数返回值通常存储在临时空间中,包括匿名对象也是如此。右值被进一步划分为纯右值和将亡值。纯右值主要指内置类型的常量值,如整型常量10或表达式a+b的结果。将亡值则涉及自定义类型的右值,包括匿名对象、类型转换过程中产生的临时对象,以及函数传值返回时生成的临时变量。这些右值的特点是生命周期短暂,通常在当前表达式结束后就会消亡。
由于右值都是临时创建且即将销毁的对象,对其进行拷贝操作显得效率低下。因此C++引入了右值引用的概念,通过移动语义直接"抢夺"这些即将消亡对象的资源,避免不必要的拷贝开销。移动构造的本质是资源所有权的转移,而非传统意义上的拷贝。这种机制特别适用于管理动态内存的自定义类型。
需要注意的是,将左值误标记为右值可能导致意外行为。编译器在处理函数返回值时可能会进行优化,消除中间临时对象的创建,直接将返回值构造到目标对象中。例如:当同时存在拷贝构造和移动构造时,编译器会根据操作对象的左值/右值属性选择最合适的构造方式。对于右值会优先选择移动构造以提高效率。
不仅仅有移动构造,还有移动赋值。
// 移动赋值
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,这里调用的移动赋值。
完美转发
#include <iostream>
using namespace std;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);//这里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;
}
运行结果:
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
- 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
- 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
std::forward<T>(t)
完美转发在传参的过程中保持了t的原生类型属性。