C++ Primer 0x10 学习笔记
Posted 鱼竿钓鱼干
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer 0x10 学习笔记相关的知识,希望对你有一定的参考价值。
📔 C++ Primer 0x10 学习笔记
推荐阅读 《C++ Primer 5th》知识点总结&练习题解
16.1 定义模板
16.1.1 函数模板
- 模板定义以关键字
template
开始,后跟一个模板参数列表(逗号分隔,不能为空) - 使用模板时需要指定模板实参,将其绑定到模板参数上
模板类型参数
- 模板类型参数前面加
typename
或class
关键字,一般使用typename
非类型模板参数
- 模板非类型参数表示一个值而非类型,我们通过一个特定的类型名而非关键字
class
或typename
来指定非类型参数。当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替,这些值必须是常量表达式 - 在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如指定数组大小
- 非类型模板参数的模板实参必须是常量表达式
inline
和constexpr
的函数模板
- 函数模板可以声明为
inline
或constexpr
的,放在模板参数列表后面
编写类型无关的代码
- 编写泛型代码的两个重要原则
- 模板中的函数参数是
const
的引用 - 函数体中的条件判断仅使用
<
- 模板中的函数参数是
- 模板程序应该尽量减少对实参类型的要求
模板编译
- 当编译器遇到一个模板定义时,并不生成代码,只有当我们实例化出一个模板的特定版本时,编译器才会生成代码
- 为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此与模板代码不同,模板的头文件通常既包含声明也包括定义
- 函数模板和类模板的成员函数的定义通常放在头文件这讴歌
模板和头文件
-
模板包含两种名字
-
不依赖于模板参数的名字
-
依赖于模板参数的名字
-
-
模板提供者保证:使用模板时,所有不依赖模板参数的名字都必须可见。当模板被实例化时,模板的定义,类模板的成员的定义也必须可见
-
模板的用户保证:用来实例化模板的所有函数、类型以及类型关联的运算符声明都是可见的
-
模板的设计这应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明
-
模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件
大多数错误在实例化期间报告
通常编译器会在三个阶段报告错误
-
编译模板本身(语法检查)
-
编译器遇到模板使用(检查实参数目,参数类型匹配等)
-
模板实例化(这个阶段才能发现类型相关的错误,看编译器怎么管理实例化,有可能链接时才报告)
-
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任
16.1.2 类模板
- 类模板是生成类的蓝图。与函数模板不同的是,编译器不能为类模板推断参数类型
模板实例化
- 实例化类模板需要提供显示模板实参
- 一类模板的每个实例都形参一个独立的类。类型
Blob<string>
与任何其他Blob
类型没有关联,也不会有特殊访问权限
在模板作用域中引用模板类型
- 为了阅读类模板的代码,应该记住类模板的名字不是一个类型名
- 一个类模板中的代码如果使用了另一个模板,通常不将一个实际类型(或值)的名字用作其模板的参数。相反,我们通常将模板自己的参数当作被使用模板的实参
类模板的成员函数
- 类模板之外的成员函数必须以关键字
template
开始,后接模板参数列表 - 默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化
在类代码内简化模板类名的使用
- 在类模板自己的作用域中,我们可以直接使用模板名而不提供实参
- 在类模板外定义其成员时,必须记住我们不再类作用域内,遇到类名才能进入类的作用域。由于返回类型位于类的作用域外,所以必须指出返回类型是一个实例化的类模板
类模板和友元
-
如果一个类模板包含非模板友元,则友元被授权可以访问所有模板实例
-
如果友元自身是模板,类可以授权给所有友元模板实例,也可以之授权给定实例
-
令模板自己的类型参数为友元,以便我们能用内置类型来实例化类
模板类型别名
- 新标准允许我们用类模板定义一个类型别名
- 我们定义一个模板类型别名时,可以固定一个或多个模板参数
类模板和static
成员
- 类模板可以声明
static
成员,每个static
数据成员有且只有一个定义,static
成员也可以定义为模板。 - 一个
static
成员函数只有在使用时才被实例化
16.1.3 模板参数
- 模板参数的名字没有什么内在含义,通常命名为T
模板参数与作用域
- 模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前
- 模板参数会隐藏外层作用域中声明的相同名字
- 但是模板内不能重用模板参数名
- 一个模板参数名在一个特定模板的参数列表只能出现一次
模板声明
- 与函数参数一样,声明中的模板参数的名字不必与定义中相同
- 一个给定模板的每个声明和定义必须有相同数量和种类(即,类型或非类型)的参数
- 一个特定文件所需的所有模板的声明通常一起放在文件开始位置,出现于任何使用这些模板的代码之前
使用类的类型成员
- 默认情况下,
C++
语言假定通过作用域运算符访问的名字不是类型 - 如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们必须使用
typename
来实现这点(不能用class
)
默认模板实参
- 新标准中我们可以为函数和类模板提供默认实参,旧标准只能为类模板提供
- 与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,他才可以有默认实参
模板默认实参与类模板
- 如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参时,就必须在模板名后加上空尖括号对
16.1.4 成员模板
- 一个类(无论是普通类还是模板类)可以包含本身是模板的成员函数。这种成员被称为成员模板
- 成员模板不能是虚函数
- 为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参
16.1.5 控制实例化
- 当模板被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板并提供i昂头的模板参数时,每个文件就会有该模板的一个实例
- 在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中可以通过显式实例化来避开这种开销
- 对每个实例化声明,在程序中某个位置必须有其显式的实例化定义
- 一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数,因此我们用来显式实例化一个类模板的类型,必须能用于模板的所有成员
16.1.6 效率与灵活性
shared_ptr
在运行时绑定删除器,使用户重载删除器更为方便unique_ptr
在编译时绑定删除器,避免了间接调用删除器的运行时开销
16.2 模板实参推断
- 对于函数模板,编译器利用其中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程称为模板实参推断
16.2.1 类型转换与模板类型参数
- 如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动地应用于这些实参。编译器通常是不会对实参进行类型转换的,而是生成一个新的模板实例
- 与往常一样,顶层
const
无论是在形参中还是实参中都会被忽略 - 在其他类型转换中,能在调用中应用于函数模板的包括如下两项
const
转换:可以将一个非const
对象的引用或指针传递给一个const
的引用或指针形参- 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针
- 另外的如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板
- 将实参传递给带模板类型的函数实参时,能够自动应用的类型只有
const
转换及函数到指针的转换 - 一个模板类型参数可以作用于多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须有相同的类型
- 如果函数参数类型不是模板从那树,则对实参进行正常的类型转换
16.2.2 函数模板显式实参
- 在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下希望允许用户控制模板实例化。当函数返回类型于参数列表中任何类型都不相同时,这种情况最常出现
指定显示模板实参
- 显式模板实参在尖括号中给出,位于函数名之后,实参列表之前
- 显式模板实参按由左至右的顺序于对应的模板参数匹配
- 只有尾部(最右)参数的显式模板实参才可以忽略,前提是它们可以从函数参数推断出来
正常类型转换应用于显式指定的实参
- 对于模板类型已经显式指定了的函数实参,也可以进行正常的类型转换
16.2.3 尾置返回类型与类型转换
-
当我们希望用户确定返回类型时,显式模板实参表示模板函数的返回类型是有效的,但会增加额外负担。我们可以使用尾置返回类型来解决这个问题
-
C++在
type_traits
头文件中提供了用于进行类型转换的标准库模板类
16.2.4 函数指针和实参推断
- 当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参
- 当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值
16.2.5 模板实参推断和引用
从左值引用函数参数推断类型
- 当一个函数参数是模板类型参数的一个左值引用时
- 只能传递一个左值给它
- 实参可以是
const
也可以不是,如果是const
,那么T
将被推断为const
类型
- 当函数参数本身是
const
时,T
的类型推断的结果不会是一个const
类型。const
已经是函数参数类型的一部分,因此它不会也是模板参数类型的一部分
从右值引用函数参数推断类型
- 当一个函数参数是一个右值引用,推断出的
T
的类型是该右值实参的类型
引用折叠和右值引用参数
C++
语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是move
这种标准库措施正确工作的基础- 第一个例外规则影响右值引用参数的推断如何进行。将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型
- 第二个例外绑定规则。如果我们间接创建一个引用的引用,则这些引用形参了折叠。
X& &
、X& &&
和X&& &
都折叠成类型X&
;但如果是类型X&& &&
折叠成X&&
- 折叠引用只能应用于间接创建的引用的引用,如类型别名或模板参数
- 两个规则导致了两个重要结果
- 如果一个函数参数是一个指向模板类型参数的右值引用,则它可以被绑定到一个左值
- (接上面的)并且如果实参是一个左值,则推断出的实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数
- 这两个规则暗示,我们可以将任意类型的实参传递给
T&&
类型的函数参数
编写接受右值引用参数的模板函数
- 右值引用通常用于:模板转发其实参或模板被重载
- 模板参数可以推断为引用类型可能对模板内代码有很大影响
- 当代码中设计的类型可能是普通类型,也可能是引用类型时,编写正确代码会变的异常困难(虽然
remove_reference
这样的类型转换类可能会有帮助
16.2.6 理解 std::move
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
return static_cast<typename remove_reference<T>::type&&>(t);
string s1("hi!"),s2;
s2 = std::move(string("bye!"));//从一个右值移动数据
s2 = std::move(s1);//正确但赋值后,s1值不确定
从一个右值移动数据
- 推断出
T
的类型为string
remove_reference
用string
进行实例化remove_reference<string>
的type
成员是string
move
的返回类型是string&&
move
的函数参数t
的类型是string&&
第二个赋值
-
推断出
T
的类型为string&
-
remove_reference
用string&
进行实例化 -
remove_reference<string&>
的type
成员是string
-
move
的返回类型是string&&
-
move
的函数参数t
的类型是string&&
-
从一个左值引用
static_cast
到一个右值引用是允许的
16.2.7 转发
- 某些函数需要将其一个或多个实参连通类型不变地转发给其他函数,在此情况下我们需要保持被转发实参的所有性质,包括实参类型是否
const
的以及实参是左值还是右值
定义能保持类型信息的函数参数
- 通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有信息(
const
属性、左右值属性) - 在调用中使用
std::forward
保持类型信息 - 当用于一个指向模板参数类型的右值引用函数参(
T&&
)时,forwad
会保持实参类型的所有细节 - 与
std::move
相同,对std::forward
不使用using
声明是个好主意
16.3 重载与模板
- 函数模板可以被另一个模板或一个非模板函数重载,名字相同的函数必须具有不同数量或类型的参数
如果涉及函数模板,则函数匹配规则会在以下几方面受到影响
-
对于一个调用,其他候选函数包括所有模板实参推断成功的函数模板实例
-
候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板
-
可行函数(模板与非模板)按类型转换来排序
-
与往常一样,如果一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是如果多个函数提供同样好的匹配,则:
- 如果同样好的函数中只有一个非模板函数,则选择此函数
- 如果同样好的函数中没有模板函数,而有多个函数模板,且其中一个模板比其他的更特例化,则选择此模板
- 否则,此调用有歧义
-
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本
-
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本
-
定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于为遇到你希望调用的函数而实例化一个你并非需要的版本
16.4 可变参数模板
- 一个可变参数模板就是接受可变数目参数的模板函数或模板类
- 可变数目的参数被成为参数包,存在两种参数包
- 模板参数包,表示零个或多个模板参数
- 函数参数包,表示零个或多个函数参数
- 用一个省略号来之处一个模板参数或函数参数表示一个包
- 当我们需要知道包中有多少个元素时,可以使用
sizeof...
运算符
16.4.1 编写可变参数函数模板
- 我们也可以使用一个
initializer_list
来定义一个可接受可变数目实参的函数,但是所有实参必须具有相同类型或者可以转为一个公共类型 - 当我们既不知道实参的数目也不知道它们的类型时,可变参数函数很有用
- 当定义可变参数版本的
print
时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归
16.4.2 包扩展
- 对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展它。当扩展一个包时我们还要提供用于每个扩展元素的模式。
- 扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号来触发扩展
- 扩展中的模式会独立地应用于包中的每个元素
16.4.3 转发参数包
- 在新标准下,我们可以组合使用可变参数模板与
forward
机制来编写函数,实现将其实参不变地传递给其他参数 - 可变参数函数通常将它们的参数转发给其他函数,一般形式如下
template<typename...Args>
void fun(Args&&...args)
work(std::forward<Args>(args)...);
16.5 模板特例化
- 有些情况通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确。此时我们可以定义类或函数模板的一个特例化版本
定义函数模板特例化
- 当我们定义一个函数模板时,必须为原模板每个模板参数都提供实参。
- 为了指出我们正在实例化一个模板,,应该用
template<>
的方式指出我们将为原模板的所有模板参数提供实参
函数重载与模板特例化
- 当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原模板的一个特殊实例提供了定义。
- 重要的是要弄清:一个特例化版本本质上是一个实例,而非函数名的一个重载版本,因此特例化不影响函数匹配
- 为了特例化一个模板,原模板声明必须在作用域中。并且,在任何使用模板实例的代码之前,特例化版本的声明也必须在在作用域中
- 对于普通类和函数丢失声明很容易发现,但是如果丢失了一个特例化版本的声明,编译器通常可以用原版本生产代码,很容易产生模板及其特例化版本声明顺序导致的错误,但又很难找
- 如果一个程序使用特例化版本,同时原模板的一个实例具有相同的模板实参集合,就会发生错误,但编译器又无法发现
- 模板及其特例化版本应该声明在同一个头文件当中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本
类模板特例化
- 必须在原模板定义所在的命名空间中特例化它
类模板部分特例化
- 与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分全部特性。一个类的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参
- 我们只能部分特例化类模板,而不能部分特例化函数模板
- 一个部分特例化版本本质是一个模板
特例化成员而不是类
- 我们可以之特例化特定成员函数而不是特例化整个模板
以上是关于C++ Primer 0x10 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章
C++ Primer 5th笔记(chap 16 模板和泛型编程)类模板部分特例化
C++ Primer 5th笔记(chap 16 模板和泛型编程)模板特例化
C++ Primer 5th笔记(chap 16 模板和泛型编程)函数指针和实参推断
C++ Primer 5th笔记(chap 16 模板和泛型编程)模板实参推断