目录
- 1. 支持特性的编译器版本
- 2. 模板表达式中空格
- 3. 空指针
- 4. auto
- 5. 统一初始化
- 6. explict
- 7. 范围for
- 8. =default,=delete
- 9. 化名模板(alias template)
- 10. using
- 11. noexcept
- 12. override
- 13. final
- 14. decltype
- 15. lambda
- 16. Variadic Templates(可变参数模板)
- 16.1 可变参数函数模板
- 16.1.1 不同类型参数的分解
- 16.1.2 同类型参数的分解
- 16.2 可变参数类模板
- 16.2.1 递归继承参数分解
- 16.2.2 递归内含参数分解
- 16.2.3 头尾元素处理方式不同
- 17. 右值引用
- 18. unorderd容器
- 19. tuple类型
1. 支持特性的编译器版本
(1)不同编译器版本支持的C++新特性可在 https://zh.cppreference.com中查看:
(2)可以在cpp程序中打印宏定义“__cplusplus”,看当前编译器支持C++ 98、C++ 11、C++ 14等。
2. 模板表达式中空格
C++ 11之前,模板表达式中两个方向相同的尖括号必须用空格隔开,否则会被编译器解析为流操作符>>。C++ 11后,无需用空格隔开。
3. 空指针
C++ 11之前,指针初始化或者处理空指针都是将其赋值为0或NULL。该方法有一种缺陷,即这种赋值下的指针可以被当作一个整数值使用,缺少了指针类型这一性质。C++ 11后,初始化或者处理空指针应该用nullptr,其附有指针类型的性质。
4. auto
auto关键字可以让编译器自动进行类型推导,主要用于代替长和复杂表达式的类型书写。
5. 统一初始化
C++中初始化变量和对象的方式有( ),{ },=。其中,{ }可以进行任何变量和对象的初始化:变量类型 变量名 {初始化参数 }。底层采用的是initialzer_list接收初始化参数。C++中编译器将{ 参数…}打包为initialzer_list对象。
-
{ }可以用来设初值:
-
{ }不允许窄化参数,否则编译器可能会warning或error:
std::initializer_list是标准库中的一个模板,所有的标准容器的构造函数都有以initializer_list为参数的构造函数。
-
通过将initializer_list作为函数的形参,可以给函数传输任意数量的参数,在函数内部可用initializer_list的迭代器对每个参数进行遍历。
-
initializer_list底层为一个array,里面存储每个参数的指针,可以将其看作一个辅助容器。
-
由于initializer_list构造函数是private,所以其只能由编译器调用并构造出initializer_list对象。
-
当具有特定个数参数的重载函数与有initializer_list参数的重载函数共同存在,并且输入该特定个数的参数时,有initializer_list参数的重载函数优先被调用。
-
如果没有接收initializer_list参数版本的function函数,也可以用{ }传递参数,只不过编译器会将{ }中的参数拆解,寻找符合调用的function。
initializer_list应用的例子:
6. explict
explict关键字一般用于修饰构造函数,阻止编译器隐式利用class A的构造函数将其他类型的数据转换为class A类型的对象。C++ 11之前,explict仅阻止编译器将单一参数(只有一个待定的参数)的构造函数作为类型隐式转换的工具。C++ 11之后,explict可以阻止有多个参数的构造函数作为类型隐式转换的工具。
我在侯捷老师在多参数验证explict的结果上有一些不一样的观点。两个红色框框在支持C++ 11版本的g++编译器是并未报错。除非将接收initializer_list参数的构造函数注释掉,才出现如上图的结果。因为复制初始化列表 P p3 = {77, 5, 44},应该首先用{77, 5, 44}生成initializer_list对象,之后需要initializer_list对象隐式转换为P对象,无论如何也不会去调用显式的explicit P(int a, int b, int c)构造函数,只会隐式调用P(initializer_list<\int>)。
#include <iostream>
using namespace std;
class P
{
public:
P(int a, int b)
{
cout<< "P(int a, int b) \n";
}
P(initializer_list<int>)
{
cout<<"P(initializer_list<int>) \n";
}
explicit P(int a, int b, int c)
{
cout<<"explicit P(int a, int b, int c) \n";
}
};
void fp(const P&) {};
int main()
{
P p1 = {77, 42};
P p3 = {77, 5, 44};
P p2 (77, 5,2);
fp( P{47, 11} );
fp( P{77,5,42});
cout<<__cplusplus<<endl;
return 0;
}
7. 范围for
范围for可用于遍历容器里面的元素,简化书写。依次从coll容器中取出元素赋值给decl变量(可能存在隐式转换,如果不想发生请将引起隐式转换的构造函数声明为explicit)。不过赋值过程是拷贝。如果需要实际操作容器中的元素,则需要将decl变量声明为引用。set、map等容器不允许通过迭代器改变元素值,因此无法使用范围for。
8. =default,=delete
C++编译器有时候会为类声明一些默认成员函数(默认构造、拷贝构造、move拷贝构造、拷贝赋值、move拷贝赋值、析构函数、操作符函数)。如果实现了这些函数的自定义版本后,编译器就不会去生成默认版本。大多数时候,我们需要声明带参数的构造函数,此时编译器就不会为其生成默认构造函数(无参)。如果此时还是希望编译器能帮我们自动生成一个默认构造(无参),则可以使用=default告诉编译器。=default可以用于默认构造、拷贝构造、move拷贝构造、拷贝赋值、move拷贝赋值、析构函数。如果类中有指针成员,那么需要自行撰写上述特殊成员函数;若无指针成员,则可以直接使用默认。
class Example
{
public:
Example() = default; // 编译器将为其生成一个默认构造函数
Example(int i) : data(i) {}
private:
int data;
};
有时候可能不希望让编译器悄悄地帮我们生成一个默认函数,C++ 11可以直接令函数=delete,告诉编译器不要帮我生成该默认函数,=delete也可以禁止某函数被使用,可用于任何成员函数。
- 禁止类型隐式转换,也可以用在普通函数上。
class Example
{
public:
Example(int i) {}
Example(char c) = delete;
};
int main()
{
Example t1(1);
// Example t2('a'); // 无法通过编译,由于重载特性,优先调用最佳匹配函数Example(char c),但该函数为delete
}
- 禁止对象被拷贝:以前可以将拷贝构造、拷贝赋值声明为private来实现禁止对象被拷贝,现在可以使用delete实现。
9. 化名模板(alias template)
化名模板是给模板起一个别名,本质上还是模板。
-
可以简化模板书写;
-
一个模板被当作另一个模板的模板参数时,可以用模板的化名,以化名将模板本身传入另一个模板的模板参数中。直接传入模板的名字或者临时对象,编译器均不认识,因为一般编译器只对实例化后的模板对象进行拷贝传递;
采用模板的化名传递模板,作为模板的模板参数:
-
不允许对化名模板做特化。
化名模板类似于模板的typedef,但比模板的typedef更强大,typedef不接受变化的参数,而化名模板可以。
10. using
using关键字可以用于展开命名空间,声明类成员。C++ 11后,using还可以被用于类型化名(类似typedef),化名模板。
11. noexcept
noexcept关键字用于表示一个函数或表达式在执行期间是否会抛出异常,相当于函数或表达式的作者对编译器和使用者承诺绝不抛出异常,从而使得编译器可以对其进行优化、方便使用者调式代码。noexcept后面可以紧跟一个条件判断结果,表示在条件判断为真的情况下,该函数绝不抛出异常。
下例表示如果x.swap(y)函数不抛出异常,外层的swap()函数就绝不会抛出异常。
当使用自动增长型的容器(例如vector、deque等)时,标准库容器为了提高效率会调用move copy,前提是容器存放的元素对象的move copy构造和move copy赋值被声明为noexcept,否则标准库容器将不会使用move copy。
12. override
override关键字表示如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译。避免程序员将需要重写的函数写错为重载。
13. final
当final关键字用于修饰类时,声明该类已经是整个继承体系的最后一种派生类了,其他类将无法继承该类。
当final关键字用于修饰虚函数时,声明该类已经是整个继承体系中最后一个能重写该虚函数的派生类了,后续继承该类的派生类中不允许在重写该虚函数了。
14. decltype
decltype可以获取对象或表达式的类型。类似的关键字有typeof,但是它不是标准库中的。
-
获取对象类型:
-
获取表达式(包括lambda表达式)类型:(下例表明返回值类型为x+y的类型)
15. lambda
lambda表达式被用来创建一个匿名函数,加上()后表示调用,可替换独立函数或者函数对象,基本上可以等效于仿函数对象。lambda表达式的格式如下:
- […]为取用外部的数据变量,里面的数据是在lambda表达式创建的时候就于其绑定好了,默认是拷贝赋值[=],[&]表示将外部的数据变量以引用的形式绑定到lambda表达式上;
- (…)为lambda表达式被调用时传入的新参数,无传入参数则可以不写(),前提是mutable、throwSpec、retType都不写;
- mutable关键字表示[…]中的数据是可以被改写的,一般[=]拷贝得到的数据均是可以改写的,因此也要配套使用mutable关键字,[&]的参数无需用mutable;
- throwSpec表示是否抛出异常;
- ->表示后面将要写返回值类型;
- retType为lambda返回值类型。
例子:
由于lambda本身是一种C++ 语法,并不是真正的类对象,没有构造函数等等。所以不建议用lambda来构造其他对象,因为如果构造其他对象时没有传入lambda实例,其他对象的构造函数可能会去调用lambda构造函数,从而导致报错。
标准库中,lambda表达式最常用在一些algorithm中,当作仿函数传入算法中进行执行,效率比真实的仿函数高一点点。
16. Variadic Templates(可变参数模板)
16.1 可变参数函数模板
在 Variadic Templates中,模板参数个数可以是不固定的,下例中,用typename T来表示模板的第一个参数,用typename… Types表示模板剩余的所有模板参数(简称模板参数包)。注意模板参数包声明时…与传入模板函数时的…位置不一样。可变参数模板根据所传入的模板参数进行实例化。
template <typename T, typename... Types>
void printx( const T& firstArg, const Types&... args){
cout << firstArg << endl;l l print first argumentprintX(args...);
}
16.1.1 不同类型参数的分解
可变参数模板有一个重要的应用,就是结合递归调用模板自身,将所有的模板参数一一分解开来,分别进行处理。下例中,print函数模板参数由一个固定模板参数T和一个不固定模板参数包Types组成,print实例中有四个模板参数,那么print第一个模板实例就是接收firstArg接收第一个参数7.5,剩下的三个参数全部由args接收。print内部又将包含剩余参数的参数包作为参数递归调用print。递归调用的第一个print将收到第一个参数为“hello”,和剩下两个参数组成的参数包,总是分解成1+若干参数…,直到递归分解直至调用终止的无参print( )函数。
- sizeof…(args)输出args参数个数;
- 在同一个函数的不同模板中,template<typename T, typename… Types>比template<typename… Types>更特化,因此上例中只会调用1和2。
16.1.2 同类型参数的分解
若需要分解的参数是同类型的,那么可以直接用initializer_list替换可变参数模板。
16.2 可变参数类模板
16.2.1 递归继承参数分解
可变参数模板也可以用在类模板中,一个常用的技巧就是通过递归继承分解模板参数。C++新特性中tuple就是利用了这中技巧。tuple容器可以将不同数据类型的元素放在一个容器中。其基本原理是将模板参数分解为1+args(若干参数),创建第一个类型参数的成员,并继承只包含args参数的模板类,从而依次递归实现不同类型参数的分离与成员变量的创建。当需要对tuple对象取元素时,直接将子类的指针转换为指向父类的指针即可。如下图所示(typename Head::type 可直接改为Head(本就是参数类型,内置类型无::type,例如无int::type)):
16.2.2 递归内含参数分解
与递归继承不同,可以采用递归内含剩余模板参数类型的类,实现分解不同模板参数并构造对应的对象。
16.2.3 头尾元素处理方式不同
当处理头尾参数与处理中间参数的方式不同时,则可以借用可变参数模板分解参数的同时,根据参数的索引序号进行特殊处理,其中是否是尾参数可用sizeof…()获取可变参数个数。
17. 右值引用
首先需要理解左值与右值:
- 左值为存储在内存中、有明确存储地址(可取地址)的数据;另一种分辨方法就是能够出现在‘=’的左边的为左值;
- 右值为可以提供数据值的数据(不可取地址);另一种分辨方法是只能出现在‘=’的右边的为右值(规定临时对象是一种右值)。
右值引用是C++ 11的新特性,它是对一个右值进行引用的类型(&&),主要为了避免不必要的拷贝造成资源的浪费和参数转发。由于右值是匿名的,因此只能通过引用它的方式找到它。通过右值引用的声明,该右值又”重获新生”(相当于给匿名对象重新起了名字且不会马上析构),其生命周期与右值引用类型变量的声明周期一样,因此“右值引用的结果”是左值。当右值出现在赋值符号‘=’的右侧时,我们认为可以让编译器将右值的资源直接搬移(偷取)给左值而不需要重新拷贝再析构右值。右值引用一般用于撰写move版本的copy构造函数和move版本的copy 赋值函数,内部实现只是将原来指向资源的指针赋值为null,新对象的指针接管原来的资源,本质上是一个浅拷贝过程,因此效率高。输入是右值且要执行拷贝动作时,优先调用move版本的拷贝构造函数。
标准库中std::move()函数可以将左值转换为右值,但是使用者必须确保后续不再使用被转换的左值(左值的资源已经被转移走了)。下例中,调用普通拷贝构造函数构造c1,调用move拷贝构造函数(右值引用)构造c2且后续不允许再使用
当存在多层函数嵌套调用时,参数的右值特性可能会被丢失。标准库中std::forward()函数用于保持参数类型信息的完美转发。
调用std::forward时,根据参数的左值或右值属性,编译器会选择适当的模板实例进行转发。下图为std::forward源码。如果输入参数是一个左值引用(int &),std::forward将返回一个左值引用(int & && 折叠为 int &)。如果参数是一个右值引用(int &&),std::forward将返回一个右值引用(int && && 折叠为int &&)。
根据模板类型推导,如果输入参数是一个左值引用(int &),__t和std::remove_reference<_Tp>::type &的类型为int &,std::remove_reference<_Tp>::typede 类型为int , 则_Tp为int & ,因此返回值转型为_Tp&&,即为int & &&,根据折叠原理折叠为int &。同理输入右值引用的到int &&。
完整的带move拷贝的类例子:
18. unorderd容器
C++ 11后,将原来的无序容器名字进行了更换。
用到底层由哈希表实现的容器时,需要传入一个计算hash值的函数对象。由于有的对象可能包含许多不同类型的成员变量,如何计算hash值存在困难。一种万用的hash function如下:
19. tuple类型
tuple是C++11新标准的类型。tuple可以存放任意个数任意类型的元素, 可以使用直接初始化, 和"make_tuple()"初始化, 访问元素使用"get<>()"方法, 注意get里面的位置信息, 必须是常量表达式(const expression);
(本文为学习侯捷老师C++2.0新特性课程后的笔记)