详解 C++11
文章目录
- 1. C++11简介
- 2. 初始化列表
- 2.1 {}
- 3. 右值引用的移动语义
- 3.1 C++中的值概念
- 3.2 左值引用与右值引用
- 3.3 移动语义
- 3.4 移动语义下的构造,赋值和插入
- 4. emplace_back
- 4.1 emplace_back 和 push_back
- 4.2 引用折叠
- 4.3 完美转发
- 4.4 可变参数模板
- 4.4.1 包扩展
- 4.5 emplace_back的实现
- 5. lambda表达式
- 5.1 lambda表达式结构
- 5.2 lambda表达式原理
1. C++11简介
C++11,是从C++98之后,一个非常重要的版本,在这个版本中,引入很多非常重要的更新:初始化列表,右值语义下的移动构造和移动赋值,emplace_back和lambda表达式等。
2. 初始化列表
这个初始化列表,不同类构造函数中的初始化列表,而是C++11引入的一种新数据结构:initializer_list。
initializer_list这种模板容器类型,构造和赋值都需使用{}这个列表结构进行,只能存储相同类型的数据,与顺序表类似。
2.1 {}
C++11中,引入初始化列表后,提出一个理念:万物皆可用{}进行初始化,即任何类型的数据定义时,用{}初始化,且可以省略赋值操作符。
{}用这个初始化,有两种情况:
- 一种是隐式类型转换成相应类型,然后再初始化。
- 另一种是,隐式类型转换成初始化列表类型,然后再用初始化列表进行初始化。当然,必须满足{}为相同类型才能转化为initializer_list,同时相应类型为自定义类型,并且提供了匹配initializer_list的接口:如果以上条件均满足,这是会优先匹配的。
3. 右值引用的移动语义
3.1 C++中的值概念
C++中将值分为两大类:泛左值和右值。
泛左值:可以进行用户层面ODR-USED的值,简单来说,就是用户可以直接取地址的值,我们都称之为泛左值。泛左值又分为:左值和将亡值。左值,主要强调生命周期存有一段时间;将亡值,则指生命周期即将结束。
右值:右值分为将亡值和纯右值。将亡值即泛左值中,纯右值则不能ODR_USED,用户层面无法取地址,通常是一些字面量或者临时对象和匿名对象。
实际上,C++中的右值概念是泛化的,传统意义上的右值应为纯右值,但是C++中,右值泛指那些字面量,或者非字面量中生命周期短暂,资源需要被转移的值。
3.2 左值引用与右值引用
&& 是右值引用,分为const和非const 版本。
& 是左值引用,同样分为const和非const版本。
需要特别注意的是,如果创建一个实际右值引用变量,那么这个变量本身具有左值属性,因为可被ODR_USED。
如果想要让一个变量具有右值属性,可以使用库函数move,本质就是实现一个类型强转功能。
任何值皆可被const&进行引用,但是右值引用只能引用严格具有右值属性的值,即纯右值,绝不能引用左值。
3.3 移动语义
什么是移动语义?移动语义是针对右值提出的。因为右值,基本为生命周期即将结束的值,而且临时对象或匿名对象,往往是一个表达式中的中间值,并不是最终结果——对于自定义类型的右值而言,尤其包含资源时,资源的深拷贝消耗很大,效率不高,而这些右值,申请了资源,却又要很快释放资源,没有意义。因此,就可以把这些右值的资源,转移给相应的左值,进而减少资源的申请和释放,这就是右值引用的移动语义。
3.4 移动语义下的构造,赋值和插入
在什么时候会出现申请资源的深拷贝呢?自定义类型的构造和赋值时。
因此,除却原有的拷贝构造和拷贝赋值,引入移动构造和移动赋值。
移动构造和移动赋值是用来进行资源转移的。一个右值对象,会优先匹配移动构造和移动赋值,其次再匹配拷贝构造和拷贝赋值。
移动构造与移动赋值,建议声明为noexcept,即不允许抛异常,因为资源交换的过程不允许被中断,否则可能产生一些问题;而拷贝构造和拷贝赋值,对异常的容忍度高,也实际存在需要catch异常的场景,因为存在new申请资源。
c++11引入移动语义后,相关插入接口基本重载了右值引用的移动版本,如:push_back和insert系列接口。
4. emplace_back
4.1 emplace_back 和 push_back
既然push_back已经重载右值版本,整体效率大大提升,为什么要引入emplace_back?
emplace_back实际是插入效率的进一步提升,在push_back需要构造+移动构造的场景下(即涉及隐式类型转换),emplace_back可以实现直接构造。
那么,emplace_back的直接构造如何实现?其中涉及引用折叠,完美转发以及可变参数模板等知识。
4.2 引用折叠
引用折叠的出现,主要是在万能引用的背景下提出的。
C++中,希望存在万能引用,即传入一个左值,通过左值引用;传入一个右值,通过右值引用——为了实现万能引用,引用折叠规则因此而生。
using type1 = int&&;
using type2 = int&;
template<class T>
void f(T&)
{}template<class T>
void f(T&&)
引用折叠出现在类型的重命名和模板中,不能自身写多个&,编译器无法解析。
对于左值引用类型,即无论&左侧无论是&,还是&&都最终解析成左值引用,所以最终传入右值或左值,均被左值引用。
对于右值引用类型,&&的左侧如果是&,解析成左值引用;如果是&&,则解析成右值引用。所以,就有万能引用:传入左值,则会自动推导出&,形成左值引用;传入右值,则自动推导出相应类型,构成右值引用(不推导出&&,虽然最终也构成右值引用)
4.3 完美转发
万能引用很方便,但是又引入新问题:因为右值引用的对象本身具有左值属性,万能引用让左值被左值引用,右值被右值引用,但如果调用函数,只能匹配到左值引用的版本。
而实际上,传入右值,是希望资源转移,此时却无法实现。不能直接move,因为左值不允许资源转移。
所以,我们希望传入后,能够保持之前传入值的类型,这样可以实现自行匹配:资源转移或不转移。因此,引入完美转发。
std::forward<T>();
forward本质是一个函数模板,实际使用时,将万能引用模板中的T类型进行函数实例化,然后将相应引用对象作为参数传入,本质也是类型强转——这样即可实现值类型保持。
顺带提一嘴,decltype关键字,decltype,给其对象,会返回对象的实际类型。
4.4 可变参数模板
C中支持可变参数,C++中同样引入了一套可变参数机制:即可变参数模板。
template<class ...Args>
void func(Args... args)
{}
上图中,func函数支持多参数传入,这些参数最终会被打包成一个参数包,参数包类型是Args,采用模板类型(因为不同的参数,打包后,决定参数包类型的不同),参数包对象是args:理解args,可以直接把其拆成多个参数分列理解,实际上起到的作用也是如此。
4.4.1 包扩展
如何对参数包进行解包,即分别拿到参数包中的每一个参数?
有两种常用的解包方式:
void ShowList()
{std::cout << “无参数” << std::endl;
}template<class T,class ...Args>
void ShowList(T x,Args... args)
{std::cout << x << std::endl;ShowList(args);//本质是在编译时递归解包,通过函数模板的自动推导和函数重载实现
}template<class ...Args>
void func(Args... args)
{ShowList(args);
}
template<class T>
const T& GetArgs(const T& x)
{std::cout << x << std::endl;
}
template<class ...Args>
void Argument(Args... args)
{
}
template<class ...Args>
void func(Args... args)
{Argument(GetArgs(args)...);//此处的含义是将args参数包中的每一个参数分别单独传给GetArgs并调用该函数,如果没有参,则就不再调用GetArgs,最终GetArgs返回的所有参数,再一起传给Argument.
}
除此之外,再介绍一个专门用于计算参数包中参数个数的操作符:sizeof…(args),给相应的参数包对象,可以得到其中参数个数。
4.5 emplace_back的实现
emplace_back的实现中:
- 借助可变参数模板,实现直接传入可变的参数,这些参数是可以用来直接构造自定义类型的对象。特别说明,push_back支持传入{},存在隐式类型转换;但由于emplace_back实现为可变参数模板,反倒不再支持传入{}。
- 借助万能引用和完美转发,使得传入的右值对象保持右值,传入的左值保持左值,进而保证能正确匹配相应构造函数。
- 为什么是直接构造?前面说过,args可以将其展开成多个参数看待,因此调用构造函数时,可以直接传入——构造函数中,如果实现可变参数模板的,则会优先进行函数包匹配,否则则将函数包展开匹配相应的构造。 对于底层的空间,如果未开辟,则直接new调用相关构造,否则使用定位new,同样是直接构造。
5. lambda表达式
lambda表达式是C++从别的语言中引入的,一种轻量化实现的可调用对象。
lambda表达式统一结构:
[capture](parameters) mutable -> return_type{ body };
lambda表达式示例:
int x = 5;
int y = 6;
auto f = [x,&y](int z) mutable -> void{std::cout << x << std::endl;std::cout << y << std::endl;std::cout << z << std::endl;
};
5.1 lambda表达式结构
lambda表达式结构与函数类似,有函数体,函数参数等。
但是lambda表达式不用写出函数名,同时存在捕捉列表[ ]结构。
lambda表达式的返回值可以显式通过 -> 指明,也可以依赖编译器自动推导。
因为lambda表达式希望能够在函数体中使用一些对象,同时又不希望通过参数形式传入,因此引入捕捉列表。
捕捉列表即对lambda表达式外部变量进行捕捉,分为值捕捉和引用捕捉。
- 值捕捉。值捕捉直接写对象名,捕捉到的对象本质是外部对象的拷贝,具有const属性,如果要修改,需要在lambda表达式传入参数表,即()后,添加mutable.
- 引用捕捉。引用捕捉使用&,捕捉到的对象是外部对象的引用,不具有const属性,可直接修改,引用会影响外部对象。
当然,也可以值捕捉和引用捕捉混用。除此之外,还有隐式捕捉和显式捕捉之分:显式,就是明确显出捕捉哪个对象;隐式,就是只表明是值捕捉 = 还是引用捕捉 & ,根据函数体中实际使用到的对象,编译器自行进行捕捉。
5.2 lambda表达式原理
每一个lambda表达式都具有自己的唯一类型,这个类型是编译器生成的,我们上层一般无法直接得知。因此,lambda表达式对象,类型一般都写成auto,类型由编译器自行推导。
在C++中,lambda表达式,底层本质就是一个仿函数,实现了对operator()
的重载。
而捕捉列表中捕捉的对象,本质就是类中成员变量:非mutable值捕捉,就是类中定义相应const 对象,并在构造中完成拷贝,加上mutable,就是去除const关键字;引用捕捉,就是类中定义相应引用对象,并在构造中完成引用。