C++11超详细解析——爆肝万字详解

时间:2024-11-05 11:36:16

1.列表初始化

1.1C++98传统的{} 

C++98中⼀般数组和结构体可以⽤{}进⾏初始化

struct Point
{
	int _x;
	int _y;
};
int main()
{
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };

	return 0;
}

1.2C++11的{}

• C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化

• 内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化 了以后变成直接构造

• {}初始化的过程中,可以省略掉=

• C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便 利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便

模拟实现一个日期类Date来做测试 

#include<iostream>
#include<vector>
using namespace std;
struct Point
{
	int _x;
	int _y;
};
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

编译器对{}初始化的优化,直接拷贝而不调用拷贝构造 

临时对象拷贝与优化拷贝构造的对比 

{}初始化的优势 

1.3 C++11中的std::initializer_list 

• 上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个 值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector v1 = {1,2,3};vector v2 = {1,2,3,4,5};

• C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷⻉ 过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。

• 这是他的⽂档:initializer_list,std::initializer_list⽀持迭代器遍历。

• 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进⾏ 初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过 std::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);

initializer-list对象的构造 

直接构造与优化构造的区别 

2.右值引用与移动引用

C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学 习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名 

2.1 左值和右值

• 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址

• 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象 等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址

• 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是leftvalue、rightvalue的缩写。现代C++中,lvalue被解释为loactor-value的缩写,可意为存储在内 存中、有明确存储地址可以取地址的对象,⽽rvalue被解释为real-value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址

左值引用

右值引用 

2.2左值引用与右值引用 

• Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名

• 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值

• 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)

• template typename remove_reference::type&& move (T&& arg);

• move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知识,这个我们后⾯会细讲

• 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值

• 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1 汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途

左值引用与右值引用

左值与右值的交叉引用 

小tips:左值引用与右值引用的本身属性都是左值 

2.3 引⽤延⻓⽣命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改 

具体的例子就是函数栈帧中存储的形参在栈帧销毁时也会同时销毁,如果需要存储不能使用左值引用来延长生命周期,但是可以使用右值引用来掠夺形参资源保存下来

2.4 左值和右值的参数匹配

• C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配

• C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)

• 右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,下⼀⼩节我们讲右值引⽤的使⽤场景时,就能体会这样设计的价值了

三个重载函数 

2.5移动构造和移动赋值

• 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值

• 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤

• 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的 右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的bit::string 样例实现了移动构造和移动赋值,我们需要结合场景理解

这里的其他函数都只是辅助所以只需要给出移动构造的结构即可

移动构造的核心就是将待销毁的对象直接掠夺其所有资源,不必拷贝效率很高

这些对象包括了临时对象 

右值对象构造,只有拷⻉构造,没有移动构造的场景  

• 图1展⽰了vs2019debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右 边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造

• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接 将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。 变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所示

• linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次拷⻉

右值对象构造,有拷⻉构造,也有移动构造的场景  

• 图2展示了vs2019debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右 边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造

• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接 将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。 要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所示

• linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动

VS2019的优化版本 

VS2022的优化版本

右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景 

• 图4左边展示了vs2019debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境 下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值

• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造 要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名

右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景 

• 图5左边展示了vs2019debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。

• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名

左值引用与右值引用的实际作用 

当需要对自定义类型深拷贝时右值引用实现的移动拷贝与移动构造就有了很大作用

2.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) 
• 泛左值(generalized-value,简称glvalue),泛左值包含将亡值和左值 
• 有名字,就是glvalue;有名字,且不能被move,就是lvalue;有名字,且可以被move,就是 xvalu;没有名字,且可以被移动,则是prvalue

2.7 引⽤折叠

• C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或typedef中的类型操作可以构成引⽤的引⽤

• 通过模板或typedef中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规 则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤

• 下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀ 个仔细理解⼀下

• 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左 值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤

• Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function

折叠规则 

2.8完美转发 

• Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数

• 但是结合我们在5.2章节的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定 后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传 递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性, 就需要使⽤完美转发实现。

• template T&& forward (typename remove_reference::type& arg);

• template T&& forward (typename remove_reference::type&& arg);

• 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回

如果不完美转发,当当前层并非最后调用引用层时,引用会不断传递,可能导致类型退化,实际样例如下:

我们可以看到这里给出了四种重载函数并且测试时将左右值引用与const引用都测试了一遍但是结果只有左值引用的重载函数被调用了,这是因为二次调用了引用导致了类型的退化

完美转发

这里的完美转发就很好的解决了类型退化的问题,当然完美转发的用途不只有这些

比如当一个函数需要调用左值与右值引用并且需要多层调用,这里就可以使用完美转发时类型始终保持不变,并且还可以使用万能引用&&来进行引用折叠,使得不论传的是左值还是右值都只用一个函数即可,去除了多余的冗余代码

3.可变参数模版 

3.1基本语法及原理

• C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称 为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数

• template<class ...Args>  void Func(Args... args) {}——传值

• template<class ...Args>  void Func(Args&... args) {}——左值引用

• template<class ...Args>  void Func(Args&&... args) {}——万能引用

• 我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出 接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表示,跟前⾯普通模板 ⼀样,每个参数实例化时遵循引⽤折叠规则

• 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数

• 这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数

可变参数模版的使用 

可变模版参数的实质

实际上就是在编译时结合引用折叠原理根据参数实例化不同参数个数的模版函数

3.2包扩展

• 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如图1所示

• C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理,下面使用一段代码解析

参考代码 

 

代码顺序 

实例化推导 

比较新颖的包扩展方法: 

 

3.3emplace系列接口解析

• C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container,empalce还⽀持 直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象

• emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列

• 第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递, 最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象

• 传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下

std::forward(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值

总结:emplace系列兼容push系列和insert的功能部分场景下emplace可以直接构造,push和insert是构造+移动构造或构造+拷贝构造所以emplace综合而言更好用更强大,推荐用emplace系列替代push和insert

4.lambda 

4.1lambda表达式语法

• lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部

lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接 收 lambda 对象

• lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }

• [capture-list] :捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来 判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉。捕捉列表为空也不能省略

• (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略

• ->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此 部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导

• {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略

书写格式: 

4.2捕捉列表

• lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就 需要进⾏捕捉

• 第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x, y,&z]表示x和y值捕捉,z引⽤捕捉

• 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表 写⼀个&表示隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量

• 第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显式捕捉。[=,&x]表⽰其他变量隐式值捕捉, x引⽤捕捉;[&x,y]表示其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是 &或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉

• lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态 局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使 ⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空

• 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改, mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以 修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)

4.3lambda的原理

• lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for 这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会⽣成⼀个对应的仿函数的类

• 仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返 回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成 的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕 捉,编译器要看使⽤哪些就传那些对象 

• 上⾯的原理,我们可以透过汇编层了解⼀下,下⾯第⼆段汇编层代码印证了上⾯的原理

代码层面:

汇编层面: 

5.包装器 

5.1function

• std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存 储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对 象被称为std::function 的⽬标。若std::function 不含⽬标,则称它为空。调⽤空std::function 的⽬标导致抛出std::bad_function_call异常

• 函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型,下⾯的第⼆个代码样例展示了 std::function 作为map的参数,实现字符串和可调⽤对象的映射表功能

 function的使用及注意事项:

function的实际应用场景:后缀求算式表达式的值(逆波兰表达式求值) 

5.2bind

• bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收 的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序

• 调⽤bind的⼀般形式: auto newCallable = bind(callable,arg_list); 其中 newCallable本⾝是⼀个可调⽤对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调⽤newCallable时,newCallable会调⽤callable,并传给它arg_list中的参数

• arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示⽣成的可调⽤对象 中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占位符放到placeholders的⼀个命名空间中

bind的语法格式: 

 bind的实际应用:计算复利