C++11 列表初始化和左值右值
一.C++11简介
我们直到,自C++98发行以来,时隔13年才发布了它的第二个大版本C++11,之后的C++基本保持三年一更新的速度,由此可见C++11引入了大量的重要更新。自本文开始将陆续介绍C++11带来的主要更新,以便我们适应企业的要求。

二.列表初始化
C++11相较于C++98对数据和对象的初始化方法更加丰富。比如{}。
1.C++11支持万物{}初始化
C++98支持如下类型的{}初始化。例如结构体,数组
int a1[] = { 1, 2, 3, 4, 5 };
int a2[5] = { 0 };
Point p = { 1, 2 }
而C++11不仅支持对常量进行{}初始化,还引入了一种叫列表初始化(initializer_list)的初始化方法。
我们用list做例子。
int main()
{
// C++98支持的
int a1[] = { 1, 2, 3, 4, 5 };
int a2[5] = { 0 };
Point p = { 1, 2 };
// C++11支持的
// 内置类型支持
int x1 = { 2 };
int x2 = 2;
// 自定义类型支持
// 这里本质是用{ 2025, 1, 1}构造一个Date临时对象
// 临时对象再去拷贝构造d1,编译器优化后合二为一变成{ 2025, 1, 1}直接构造初始化d1
// 运行一下,我们可以验证上面的理论,发现是没调用拷贝构造的
Date d1 = { 2025, 1, 1 };
Date d20(2025, 1, 1);
// 这里d2引用的是{ 2024, 7, 25 }构造的临时对象
const Date& d2 = { 2024, 7, 25 };
// 需要注意的是C++98支持单参数时类型转换,也可以不用{}
Date d3 = { 2025 }; // C++11
Date d4 = 2025; // C++98
string str = "1111";
// 可以省略掉=
Point p1{ 1, 2 };
int x3{ 2 };
Date d6{ 2024, 7, 25 };
const Date& d7{ 2024, 7, 25 };
// 不支持,只有{}初始化,才能省略=
// Date d8 2025;
vector<Date> v;
v.push_back(d1);
v.push_back(Date(2025, 1, 1));
// 比起有名对象和匿名对象传参,这里{}更有性价比
v.push_back({ 2025, 1, 1 });
map<string, string> dict;
dict.insert({ "xxx", "yyyy" });
// 构造+拷贝构造+优化
//vector<int> v1 = { 1,2,3,4 };
//vector<int> v2 = { 10,20,30,1,1,1,1,1,1,1,1,1};
//const vector<int>& v4 = { 10,20,30,1,1,1,1,1,1,1,1,1 };
vector<int> v1{ 1,2,3,4 };
vector<int> v2{ 10,20,30,1,1,1,1,1,1,1,1,1 };
const vector<int>& v4 { 10,20,30,1,1,1,1,1,1,1,1,1 };
// 构造
vector<int> v3({ 10,20,30,1,1,1,1,1,1,1,1,1 });
//实际上initializer_list的原理与数组是有一定相似之处的
initializer_list<int> il1 = { 10, 20, 30, 1, 1, 1, 1, 1, 1, 1, 1, 1};
int aa1[] = { 10, 20, 30, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
// initializer_list + {}pair初始化隐式类型转换
map<string, string> dict = { { "xxx", "yyyy" }, { "sort", "zzzz" } };
return 0;
}
STL的容器中都添加了initializer_list构造。
// STL中的容器都增加了⼀个initializer_list的构造
vector (initializer_list<value_type> il, const allocator_type& alloc =
allocator_type());
list (initializer_list<value_type> il, const allocator_type& alloc =
allocator_type());
map (initializer_list<value_type> il,const key_compare& comp =
key_compare(),const allocator_type& alloc = allocator_type());
// ...
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
for (auto e : l)
push_back(e)
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
// 另外,容器的赋值也⽀持initializer_list的版本
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);
三.右值引用与移动语义
1.左值和右值
1.左值和右值的概念跟赋值符号有关。左值能在赋值符号的左边也能在右边,而右值只能在赋值符号右边。
2.从性质上来讲,我们可以把左值看作一个放着数据的容器——它既可以取这个容器(地址),也可以取这个数据(使用数据),例如:这里的p,b,c,*p都是左值。
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
2.同样地,我们可以把右值单单看作一个数据,我们只能取它的数据而无法取到它的地址。并且相比之下我们会很容易发现,右值往往不是一个持续存在的数据,例如:
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");
可以简单总结出
| 特性 | 左值 (lvalue) | 右值 (rvalue) |
|---|---|---|
| 核心含义 | 有标识、持久的内存位置 | 临时、短暂的值 |
| 能否取地址 | 可以 (&a) | 通常不可以 |
| 赋值运算符左边 | 可以 | 不可以 |
| 比喻 | 容器 | 容器里的内容 |
| 引用类型 | T& (左值引用) | T&& (右值引用) |
| 主要用途 | 存储数据,可以被修改和读取 | 用于表达式计算、作为函数返回值、用于移动语义 |
2.左值引用与右值引用
1.右值引用引入的原因
左值引用的作用大家都很清楚,它的底层是指针,它允许我们使用左值引用作为函数的参数和返回值,这一操作减少了因拷贝产生的内存消耗,在实践中有着非凡意义。同样地,右值引用的引入也能减少对字符串类型拷贝产生的内存消耗,除此之外,右值引用和移动语义,完美转发等功能也密不可分。
2.右值引用的形式
• Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
• 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
• 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知
识,这个我们后⾯会细讲。
• 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值。要特别注意这个性质,下面会详细介绍。
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{ // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
#include<iostream>
using namespace std;
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
double x = 1.1, y = 2.2;
// 左值引⽤给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
// 右值引⽤给右值取别名
int && rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
// b、r1、rr1都是变量表达式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;
// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
int& r6 = r1;
// int&& rrx6 = rr1;
int&& rrx6 = move(rr1);
return 0;
}
3.右值引用可以延长生命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。右值又称“将亡值”的原因在这里就可以体现出来,因为它们代表的多是一些临时对象(如一个表达式产生的临时对象,函数返回值产生的临时对象等),它们的生命周期只在当前行,而用右值引用绑定右值后,在当前栈帧的生命周期就会被延长。
int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改
std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改
std::cout << r3 << '\n';
return 0;
}
4.左值和右值的参数匹配
• C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
• C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
• 右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,下⼀⼩节我们讲右值引⽤的使⽤场景时,就能体会这样设计的价值了。这里简单介绍一下它的设计逻辑
#include <iostream>
#include <utility>
void process(std::string& s) {
std::cout << "process左值: " << s << std::endl;
}
void process(std::string&& s) {
std::cout << "process右值: " << s << std::endl;
}
int main() {
std::string hello = "Hello";
// 情况一:直接传递临时对象(右值)
process(std::string("World")); // 调用 process(std::string&&)
// 情况二:传递一个有名变量(左值)
process(hello); // 调用 process(std::string&)
// 情况三:传递一个右值引用变量
std::string&& rref = std::move(hello); // rref 是一个右值引用类型的变量
process(rref); // 调用了哪个?答案是:process(std::string&)
// 情况四:使用std::move将左值(包括右值引用变量)显式转换为右值
process(std::move(rref)); // 调用 process(std::string&&)
process(std::move(hello)); // 调用 process(std::string&&)
}
5.右值引用和移动语义的使用
左值引用的主要使用场景
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数。因为引用底层依然是指针,用引用返回一个临时对象会造成跟野指针一样的问题。因此在右值引用+移动语义的概念出现之前,只能用传值返回,但是这里返回一个字符串的消耗其实是比较大的。
class Solution {
public:
// 传值返回需要拷贝
wjh::string addStrings(bit::string num1, bit::string num2) {
bit::string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
//cout << &str << endl;
return str;
}
};
1.移动构造和移动赋值
• 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
• 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
• 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有
意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的
右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从而提⾼效率。下⾯的string
样例实现了移动构造和移动赋值。
•其实从这里的例子就能看出来为什么给右值和移动语义进行匹配,比如这里的移动构造,本身是掠夺资源,但是掠夺的是一个“将亡”值的资源,反正放着不管它的生命周期也会结束,不如为我所用。
•回填上面的坑:为什么右值引用类型为左值。我们会发现,s指向的对象是一个右值没问题,但是s本身是一个左值,为的是方便swap这里进行修改(掠夺资源),这一切的设计都是逻辑自洽的。
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
void swap(string& ss)
{
::swap(_str, ss._str);
::swap(_size, ss._size);
::swap(_capacity, ss._capacity);
}
// 移动构造
//string(string&& s)
//{
// cout << "string(string&& s) -- 移动构造" << endl;
// // 转移掠夺你的资源
// swap(s);
//}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" <<
endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
//cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
写一段代码就可以证明这里移动语义做的工作了
int main()
{
//string ret = Solution().addStrings("11111111111111111111", "222222222222222222222222222");
//cout << ret << endl;
wjh::string s1("11111111111111111");
wjh::string s3 = s1;
wjh::string s4 = wjh::string("222222222");
wjh::string s5 = move(s1);
return 0;
}
这里的s3=s1调用了移动语义,观察地址发现s1的资源被掠夺。

2移动构造和移动赋值能节省资源的原因
让我们通过一个管理动态数组的简单类来对比这两种操作:
拷贝构造/拷贝赋值
效率分析:
-
需要分配新的内存块
-
需要复制所有数据(时间复杂度 O(n))
-
对于大型对象,这是一项昂贵的操作
class Vector {
int* data;
size_t size;
public:
// 拷贝构造函数 - 深度复制
Vector(const Vector& other) : size(other.size) {
data = new int[size]; // 1. 分配新内存
std::copy(other.data, other.data + size, data); // 2. 复制所有数据
std::cout << "拷贝构造: 分配并复制了 " << size << " 个元素" << std::endl;
}
// 拷贝赋值运算符 - 深度复制
Vector& operator=(const Vector& other) {
if (this != &other) {
delete[] data; // 1. 释放现有资源
size = other.size;
data = new int[size]; // 2. 分配新内存
std::copy(other.data, other.data + size, data); // 3. 复制所有数据
std::cout << "拷贝赋值: 分配并复制了 " << size << " 个元素" << std::endl;
}
return *this;
}
};
移动构造/移动赋值
-
不需要分配新内存
-
不需要复制数据(只需复制几个指针和整数)
-
时间复杂度为 O(1),与数据量无关
class Vector {
int* data;
size_t size;
public:
// 移动构造函数 - 资源转移
Vector(Vector&& other) noexcept
: data(other.data), size(other.size) { // 1. 直接接管资源
other.data = nullptr; // 2. 置空源对象
other.size = 0;
std::cout << "移动构造: 转移了 " << size << " 个元素的所有权" << std::endl;
}
// 移动赋值运算符 - 资源转移
Vector& operator=(Vector&& other) noexcept {
if (this != &other) {
delete[] data; // 1. 释放现有资源
data = other.data; // 2. 接管新资源
size = other.size;
other.data = nullptr; // 3. 置空源对象
other.size = 0;
std::cout << "移动赋值: 转移了 " << size << " 个元素的所有权" << std::endl;
}
return *this;
}
};
移动语义的优势:
-
避免不必要的内存分配:移动操作直接接管现有资源,无需分配新内存
-
避免数据复制:移动操作只需复制指针等元数据,不需要复制实际数据
-
常数时间复杂度:移动操作的时间复杂度是 O(1),与数据量无关
-
适用于临时对象:完美处理函数返回值等场景中的临时对象
-
支持不可拷贝对象:对于像
std::unique_ptr这样不可拷贝但可移动的对象,移动语义是唯一的所有权转移方式
3.右值引用和移动语义解决传值返回
右值对象构造,只有拷⻉构造,没有移动构造的场景
• 图1展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造。
• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
• linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次拷⻉。
右值对象构造,有拷⻉构造,也有移动构造的场景
• 图2展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。
• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
• linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动。
图3右边的情况:实际上并没有产生str的对象,而是返回ret对象的引用
右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景
• 图4左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境
下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值。
• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
• 图5左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境
下编译器的处理,⼀次移动构造,⼀次移动赋值。
• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
4.右值引用和移动语义在传参中的应用
传统的传参方式:
1.按值传递 (Pass by Value)
void processVector(std::vector<int> vec) {
// 处理vec...
}
std::vector<int> large_data(1000000, 42);
processVector(large_data); // 发生拷贝构造,性能低下!
这种情况下可以通过输出型参数初步优化拷贝问题,但其实这样的解决办法使得代码可读性有一定下降
int main()
{
vector<vector<int>> ret = Solution().generate(100);
vector<vector<int>> ret;
Solution().generate(100, ret);
/*bit::string ret;
Solution().addStrings("1111111", "222222", ret);*/
return 0;
}
int main()
{
return 0;
}
2.按const引用传递 (Pass by const Reference)
优点是很高效,但值不可被修改
void processVector(const std::vector<int>& vec) {
// 可以读取vec但不能修改
// int x = vec[0]; // OK
// vec[0] = 42; // 错误:const引用
}
std::vector<int> large_data(1000000, 42);
processVector(large_data); // 无拷贝,高效
3.右值引用传参
void processVector(std::vector<int>&& vec) {
// 可以修改vec,且知道它是临时对象
vec.push_back(42); // 高效,因为我们知道vec是临时的
}
// 使用方式
processVector(std::vector<int>{1, 2, 3}); // 传递临时对象
processVector(std::move(existing_vector)); // 传递明确要移动的对象
5.重载函数同时支持左值和右值
class DataProcessor {
public:
// 处理左值(拷贝)
void process(const std::string& data) {
std::string local_copy = data; // 需要拷贝
// 处理local_copy...
}
// 处理右值(移动)
void process(std::string&& data) {
std::string local_data = std::move(data); // 高效移动
// 处理local_data...
}
};
// 使用
DataProcessor processor;
std::string data = "some data";
processor.process(data); // 调用左值版本,可能发生拷贝
processor.process(std::move(data)); // 调用右值版本,高效移动
processor.process("temporary"); // 调用右值版本,高效
6.类型分类
• C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值
(expiring value,简称xvalue)。
• 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: 42、
true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调⽤,或者整
形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于
C++98中的右值。
• 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如
move(x)、static_cast<X&&>(x)
• 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
• 值类别 - cppreference.com和 Value categories这两个关于值类型的中⽂和英⽂的官⽅⽂档,有兴趣可以了解细节。

7.引用折叠
引用折叠是一套规则,它规定了当我们在类型推导的上下文中(例如模板或 auto)间接地创建引用的引用时,编译器应如何处理这些引用。
为什么需要它?
在 C++11 之前,引用的引用是非法的,直接写 int& & 会编译错误。但引入右值引用和模板后,这种情况很容易“意外”发生。例如在模板推导中,编译器需要一套明确的规则来处理这种情况。
规则内容
规则非常简单,只有四条:
| 第一个引用 | 第二个引用 | 最终结果 |
|---|---|---|
& | & | & |
& | && | & |
&& | & | & |
&& | && | && |
简单来说:只要两个引用中有任何一个左值引用 (&),结果就是左值引用 (&)。只有两个都是右值引用 (&&),结果才是右值引用 (&&)。
发生场景
引用折叠主要发生在两种场合:
-
模板类型推导: 使用通用引用时。
-
typedef或using别名: 当别名代表一个引用类型时。 -
decltype: 在某些情况下也会发生
• 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤。
• Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模
板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的
Function,实参是右值,实例化出右值引⽤版本形参的Function。这样的函数又被叫做万能模板。
/ 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有折叠->实例化为void f1(int& x)
f1<int>(n);
f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x)
f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x)
f2<int&&>(n); // 报错
f2<int&&>(0);
return 0;
}
8.完美转发
完美转发要解决的是一个非常具体的问题:如何在一个函数模板中,将参数完全按照传入时的类型(包括左值/右值、const/volatile 等属性)转发给另一个函数?
• Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化
以后是右值引⽤的Function函数。
• 但是由上面的讲解我们知道,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定
后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传
递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数,这就是所谓的右值属性退化的现象。一些函数会将参数传递给下一个类或方法,若要传递原本的数据类型,就需要用到完美转发
| template <class T> T&& forward (typename remove_reference<T>::type& |
| arg); |
| template <class T> T&& forward (typename |
| remove_reference<T>::type&& arg); |
• 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给
Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给
Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤
返回。
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
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<class T>
void Function(T&& t)
{
Fun(t);
//Fun(forward<T>(t));
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&
t)
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&
t)
Function(std::move(b)); // const 右值
return 0;
}
我们不妨可以看看完美转发在模拟实现List的insert函数中的作用。
List作为一个容器当然需要支持插入一个右值(以及左值),于是我们把insert写成“万能模板”的形式——传左值,折叠为左值,传右值,就折叠为右值。因为insert最终会返回一个iterator类型的数据,我们就需要用到完美转发了,因为其他地方可能会用到这个数据。
template<class X>
iterator insert(iterator pos, X&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(forward<X>(x));
// prev newnode cur
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode;
}
更多推荐

所有评论(0)