侯捷 C++11/14 笔记
Variadic Template
概述
可变参数模板。
谈的是模板Templates:
- 函数模板
- 类模板
变化的是模板参数:
- 参数个数:利用参数个数逐一递减的特性,实现递归函数的调用,使用函数模板完成。
- 参数类型:利用参数个数逐一递减以致参数类型也逐一递减的特性,实现递归继承或递归复合,以类模板完成。
1 |
|
注意三种不同的
...
的应用环境,这些都是语法规则,所以记住即可。还要注意的是,在可变模板参数内部可以使用
sizeof...(args)
得到实参的个数。如果同时定义了:
1 |
|
该函数重载了void print()
,void print(const T& firstArg, const Types&... args)
是其特化版本,编译器会优先调用特化版本。
应用
实现函数的 递归调用
举了一个unordered容器中hash函数的计算例子:万用的哈希函数,函数入口
return hash_val(c.fname, c.lname, c.no)
;
1 |
|
- 实现递归继承
1 |
|
模板表达式中的空格
C++11可以去掉模块表达式前面的空格。
nullptr
标准库允许使用nullptr取代0或者NULL来对指针赋值。
- nullptr 是个新关键字
- nullptr 可以被自动转换为各种 pointer 类型,但不会被转换为任何整数类型
- nullptr的类型为std::nullptr_t,定义于
头文件中
1 |
|
auto
- C++11 auto可以进行自动类型推导。
- C语言默认的局部变量是auto类型的
- C++11 auto可以进行自动类型推导
- 使用auto的场景:类型太长或者类型太复杂
一致性初始化(uniform initialization)
C++11之前初始化时存在多个版本 {},(),=
。让使用者使用时比较混乱,C++11提供一种万用的初始化方法,就是使用大括号{}
。
原理解析:当编译器看到大括号包起来的东西{t1,t2...tn}
时,会生成一个initializer_list<T>
(initializer_list
关联至一个array<T,n>
)。调用函数(例如构造函数ctor
)时该array
内的元素可被编译器分解逐一传给函数;元素逐一分解传递给函数进行初始化。
但是如果调用函数自身提供了initializer_list<T>
参数类型的构造函数时,则不会分解而是直接传过去。直接整包传入进行初始化。所有的容器都可以接受这样的参数。
Initializer_list
1 |
|
initializer_list<T>
使用举例:initializer_list<T>
是一个class
(类模板),这个必须类型要一致,跟模板不定的参数类型相比,模板不定的参数类型可以都不一样。initializer_list<T>
类似于容器的使用方法
initializer_list
源码剖析:initializer_list<T>
背后有array
数组支撑,initializer_list
关联一个array<T,n>
initializer_list<T>
包含一个指向array
的指针,它的拷贝只是一个浅拷贝,比较危险,两个指针指向同一个内存。
initializer_list在STL中的使用:
- 所有容器都接受指定任意数量的值用于构造或赋值或者
insert()
或assign()
。 - 算法
max()
和min()
也接受任意参数。
- 所有容器都接受指定任意数量的值用于构造或赋值或者
explict
explicit
关键字一直存在,只能作用在构造函数中,目的是阻止编译器进行不应该允许的构造函数进行隐式转换。声明为explicit
的构造函数不能进行隐式转换,只能允许使用者明确调用构造函数。
C++11之前,只有non-explicit one argument
的构造函数才能进行隐式转换,2.0之后支持more than one argument
的构造函数的隐式转换。
基于范围的for循环
1 |
|
基于范围的for
循环对于explicit
类型申明的转换是不可以的。
= default, = delete
在 C++ 中,如果自定义了 big-five 函数,编译器就不会再生成默认的相关函数,但是如果我们在后边加上= default
关键字,就可以重新获得并使用编译器为我们生成的默认函数(显式缺省:告知编译器即使自己定义了也要生成函数默认的缺省版本);
=delete
关键字相对于上面来说则是相反的,=delete
表示不要这个函数,就是说这个函数已经删除了不能用了,一旦别人使用就会报错(显式删除:告知编译器不生成函数默认的缺省版本),引进这两种新特性的目的是为了增强对“类默认函数的控制”,从而让程序员更加精准地去控制默认版本的函数。
补充:
1、编译器提供的默认函数:
C++中,当我们设计与编写一个类时,若不显著申明,则类会默认为我们提供如下几个函数:
- 构造函数(
A()
) - 析构函数(
~A()
) - 拷贝构造函数(
A(A&)
) - 拷贝赋值函数(
A& operator=(A&)
) - 移动构造函数(
A(A&&)
) - 移动赋值函数(
A& operator=(A&&)
)
注意:拷贝函数如果涉及指针就要区分浅拷贝(指针只占4字节,浅拷贝只把指针所占的那4个字节拷贝过去)和深拷贝(不仅要拷贝指针所占的字节,还要把指针所指的东西也要拷贝过去);
默认提供全局的默认操作符函数:
operator
operator &
operator &&
operator *
operator->
operator->*
operator new
operator delete
2、何时需要自定义big-three(构造函数、拷贝构造、拷贝赋值)/big-five(新增移动构造函数、移动赋值函数)?
如果类中带有pointer member
(指针成员),那我们就可以断定必须要给出 big-three ;
如果不带,绝大多与情况下就不必给出 big-three 。
3、default
、delete
关键字使用示例
在c++中,如果你自定义了big-five函数,编译器就不会再为你生成默认的相关函数,但是如果我们在后边加上= default
关键字,就可以重新获得并使用编译器为我们生成的默认函数(显式缺省:告知编译器即使自己定义了也要生成函数默认的缺省版本);
=delete
关键字相对于上面来说则是相反的,=delete
表示不要这个函数,就是说这个函数已经删除了不能用了,一旦别人使用就会报错(显式删除:告知编译器不生成函数默认的缺省版本),引进这两种新特性的目的是为了增强对“类默认函数的控制”,从而让程序员更加精准地去控制默认版本的函数。
Alias Template
1 |
|
Alias Template 无法特化。
应用实例(引出模板模板参数)
考虑这样一种需求,假设我们需要实现一个函数test_moveable(容器对象,类型对象)
,从而能实现传入任意的容器和类型,都能将其组合为一个新的东西:容器<类型>,这样的话我们的函数应该怎么设计呢?
(1)解法一:函数模板(无法实现)
1 |
|
(2)解法二:函数模板+iterator+traits(可以实现)
1 |
|
这样做是可以达到效果的,但是却改变了函数签名,使用的时候我们需要这样调用:test_moveable(list<int>())
,和我们开始设计的是不一样的。那么,有没有 template 语法能够在模板接受一个 template 参数 Container 时,当 Container 本身又是一个 class template ,能取出 Container 的template 参数?例如收到一个vector<string>
,能够取出其元素类型string
?那么这就引出了模板模板参数的概念。也就是下面的解法三。
(3)解法三:模板模板参数 + alias template(可以实现)
1 |
|
这是因为 vector
其实有两个模板参数,虽然第二个有默认值,我们平时也可以像vector<int>
这样用。但是在模板中直接这样写类型是不匹配的( Container
只有一个模板参数 )。所以这里就用到了我们一开始提到的模板别名,只要传入的是vector
的模板别名就可以了,如下所示:
1 |
|
Type Alias
类型别名类似于typedef。
1 |
|
using
的用法:
- 打开命令空间或者命令空间的成员
- 类似第一种,打开类的成员
- 类型别名和模板别名(C++ 11开始支持)
1 |
|
noexpect
保证该函数不会丢出异常,可以在后面加上条件,也就是说在某种条件满足情况下,不会抛出异常。
1 |
|
一般异常处理流程:当程序发生异常时会将异常信息上报返回给调用者,如果有异常处理则处理,如果该调用者没有处理异常则会接着上报上一层,若到了最上层都没有处理,就会调用std::terminate()->std::abort()
,然后终止程序。
移动构造函数和移动赋值函数。如果构造函数没有noexcept
,vector
将不敢使用它。
override
override
用于明确要重写父类的虚函数上,相当于告诉编译器这个函数就是要重写父类虚函数这样一个意图,让编译器帮忙检查,而没有这个关键字,编译器是不会帮你检查的。
final
final
新增两种功能:
- 禁止基类被继承
- 禁止虚函数被重写
decltype
decltype 定义
引入新关键字decltype
可以让编译器找出表达式的类型,为了区别typeof
,以下做一个概念区分:
typeof
是一个一元运算,放在一个运算数之前,运算数可以是任意类型,非常依赖平台,已过时,由decltype
代替;理解为:我们根据typeof()
括号里面的变量,自动识别变量类型并返回该类型;typedef
:定义一种类型的别名,而不只是简单的宏替换;define
:简单的宏替换;typeid()
返回变量类型的字符串,用于print
变量类型。
decltype 用法
- 用来声明函数的返回值类型,一种新的指定函数返回值类型的方式;
1 |
|
- 模板之间的应用
用来求 lambda 表达式的类型
lambda 是匿名的函数对象或仿函数,每一个都是独一无二的;如果需要声明一个这种对象的话,需要用模板或者 auto ;如果需要他的 type ,可以使用 decltype ;lambda 没有默认构造函数和析构函数。
Lambdas
lambda 语法以及调用方式
定义: lambda 是一组功能的组合定义, lambda 可以定义为内联函数,可以被当做一个参数或者一个对象,类似于仿函数。
最简单的形式:
1 |
|
完整形式:
含义 | |
---|---|
[] | lambda 导入器,取用外部变量 |
() | 类似函数参数 |
mutable | []中的导入数据是否可变 |
throwSpec | 抛出异常 |
retType | 类似函数返回值 |
{} | 类似函数体 |