目录
第十三章 拷贝控制
拷贝控制操作(copy control):
-
拷贝构造函数(copy constructor)
-
拷贝赋值运算符(copy-assignment operator)
-
移动构造函数(move constructor)
-
移动赋值函数(move-assignement operator)
-
析构函数(destructor)
拷贝、赋值和销毁
拷贝构造函数
-
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
-
class Foo{ public: Foo(const Foo&); }
-
合成的拷贝构造函数(synthesized copy constructor):会将参数的成员逐个拷贝到正在创建的对象中。
-
拷贝初始化:
-
将右侧运算对象拷贝到正在创建的对象中,如果需要,还需进行类型转换。
-
通常使用拷贝构造函数完成。
-
string book = "9-99";
-
出现场景:
-
用
=
定义变量时。 -
将一个对象作为实参传递给一个非引用类型的形参。
-
从一个返回类型为非引用类型的函数返回一个对象。
-
用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。
-
-
拷贝赋值运算符
-
重载赋值运算符:
-
重写一个名为
operator=
的函数. -
通常返回一个指向其左侧运算对象的引用。
-
Foo& operator=(const Foo&);
-
-
合成拷贝赋值运算符:
-
将右侧运算对象的每个非
static
成员赋予左侧运算对象的对应成员。
-
析构函数
-
释放对象所使用的资源,并销毁对象的非
static
数据成员。 -
名字由波浪号接类名构成。没有返回值,也不接受参数。
-
~Foo();
-
调用时机:
-
变量在离开其作用域时。
-
当一个对象被销毁时,其成员被销毁。
-
容器被销毁时,其元素被销毁。
-
动态分配的对象,当对指向它的指针应用
delete
运算符时。 -
对于临时对象,当创建它的完整表达式结束时。
-
-
合成析构函数:
-
空函数体执行完后,成员会被自动销毁。
-
注意:析构函数体本身并不直接销毁成员。
-
三/五法则
-
需要析构函数的类也需要拷贝和赋值操作。
-
需要拷贝操作的类也需要赋值操作,反之亦然。
使用=default
-
可以通过将拷贝控制成员定义为
=default
来显式地要求编译器生成合成的版本。 -
合成的函数将隐式地声明为内联的。
阻止拷贝
-
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
-
定义删除的函数:
=delete
。 -
虽然声明了它们,但是不能以任何方式使用它们。
-
析构函数不能是删除的成员。
-
如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的。
-
老版本使用
private
声明来阻止拷贝。
拷贝控制和资源管理
-
类的行为可以像一个值,也可以像一个指针。
-
行为像值:对象有自己的状态,副本和原对象是完全独立的。
-
行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。
-
交换操作
-
管理资源的类通常还定义一个名为
swap
的函数。 -
经常用于重排元素顺序的算法。
-
用
swap
而不是std::swap
。
对象移动
-
很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
-
在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
-
标准库容器、
string
和shared_ptr
类既可以支持移动也支持拷贝。IO
类和unique_ptr
类可以移动但不能拷贝。
右值引用
-
新标准引入右值引用以支持移动操作。
-
通过
&&
获得右值引用。 -
只能绑定到一个将要销毁的对象。
-
常规引用可以称之为左值引用。
-
左值持久,右值短暂。
move函数:
-
int &&rr2 = std::move(rr1);
-
move
告诉编译器,我们有一个左值,但我希望像右值一样处理它。 -
调用
move
意味着:除了对rr1
赋值或者销毁它外,我们将不再使用它。
移动构造函数和移动赋值运算符
-
移动构造函数:
-
第一个参数是该类类型的一个引用,关键是,这个引用参数是一个右值引用。
-
StrVec::StrVec(StrVec &&s) noexcept{}
-
不分配任何新内存,只是接管给定的内存。
-
-
移动赋值运算符:
-
StrVec& StrVec::operator=(StrVec && rhs) noexcept{}
-
-
移动右值,拷贝左值。
-
如果没有移动构造函数,右值也被拷贝。
-
更新三/五法则:如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。
-
移动迭代器:
-
make_move_iterator
函数讲一个普通迭代器转换为一个移动迭代器。
-
-
建议:小心地使用移动操作,以获得性能提升。
右值引用和成员函数
-
区分移动和拷贝的重载函数通常有一个版本接受一个
const T&
,而另一个版本接受一个T&&
。 -
引用限定符:
-
在参数列表后面防止一个
&
,限定只能向可修改的左值赋值而不能向右值赋值。
-
第十四章 重载运算与类型转换
基本概念
-
重载运算符是具有特殊名字的函数:由关键字
operator
和其后要定义的运算符号共同组成。 -
当一个重载的运算符是成员函数时,
this
绑定到左侧运算对象。动态运算符符函数的参数数量比运算对象的数量少一个。 -
只能重载大多数的运算符,而不能发明新的运算符号。
-
重载运算符的优先级和结合律跟对应的内置运算符保持一致。
-
调用方式:
-
data1 + data2;
-
operator+(data1, data2);
-
-
是否是成员函数:
-
赋值(
=
)、下标([]
)、调用(()
)和成员访问箭头(->
)运算符必须是成员。 -
复合赋值运算符一般来说是成员。
-
改变对象状态的运算符或者和给定类型密切相关的运算符通常是成员,如递增、解引用。
-
具有对称性的运算符如算术、相等性、关系和位运算符等,通常是非成员函数。
-
运算符:
可以被重载 | 不可以被重载 |
---|---|
+ , - , * , / , % , ^ | :: , .* , . , ? : , |
& , | , ~ , ! , , , = | |
< , > , <= , >= , ++ , -- | |
<< , >> , == , != , && , || | |
+= , -= , /= , %= , ^= , &= | |
|=, *= , <<= , >>= , [] , () | |
-> , ->* , new , new[] , delete , delete[] |
输入和输出运算符
重载输出运算符<<
-
第一个形参通常是一个非常量的
ostream
对象的引用。非常量是因为向流中写入会改变其状态;而引用是因为我们无法复制一个ostream
对象。 -
输入输出运算符必须是非成员函数。
重载输入运算符>>
-
第一个形参通常是运算符将要读取的流的引用,第二个形参是将要读取到的(非常量)对象的引用。
-
输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
算数和关系运算符(+、-、*、/)
-
如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算数运算符。
相等运算符==
-
如果定义了
operator==
,则这个类也应该定义operator!=
。 -
相等运算符和不等运算符的一个应该把工作委托给另一个。
-
相等运算符应该具有传递性。
-
如果某个类在逻辑上有相等性的含义,则该类应该定义
operator==
,这样做可以使用户更容易使用标准库算法来处理这个类。
关系运算符
-
如果存在唯一一种逻辑可靠的
<
定义,则应该考虑为这个类定义<
运算符。如果同时还包含==
,则当且晋档<
的定义和++
产生的结果一直时才定义<
运算符。
赋值运算符=
-
我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
-
赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这么做。这两类运算符都应该返回左侧运算对象的引用。
下标运算符[]
-
下标运算符必须是成员函数。
-
一般会定义两个版本:
-
1.返回普通引用。
-
2.类的常量成员,并返回常量引用。
-
递增和递减运算符(++、--)
-
定义递增和递减运算符的类应该同时定义前置版本和后置版本。
-
通常应该被定义成类的成员。
-
为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
-
同样为了和内置版本保持一致,后置运算符应该返回递增或递减前对象的值,而不是引用。
-
后置版本接受一个额外的,不被使用的
int
类型的形参。因为不会用到,所以无需命名。
成员访问运算符(*、->)
-
箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
-
重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
-
解引用和乘法的区别是一个是一元运算符,一个是二元运算符。
函数调用运算符
-
可以像使用函数一样,调用该类的对象。因为这样对待类同时也能存储状态,所以与普通函数相比更加灵活。
-
函数调用运算符必须是成员函数。
-
一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
-
如果累定义了调用运算符,则该类的对象称作函数对象。
lambda
是函数对象
-
lambda
捕获变量:lambda
产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。
标准库定义的函数对象
标准库函数对象:
算术 | 关系 | 逻辑 |
---|---|---|
plus<Type> | equal_to<Type> | logical_and<Type> |
minus<Type> | not_equal_to<Type> | logical_or<Type> |
multiplies<Type> | greater<Type> | logical_not<Type> |
divides<Type> | greater_equal<Type> | |
modulus<Type> | less<Type> | |
negate<Type> | less_equal<Type> |
-
可以在算法中使用标准库函数对象。
可调用对象与function
标准库function类型:
操作 | 解释 |
---|---|
function<T> f; | f 是一个用来存储可调用对象的空function ,这些可调用对象的调用形式应该与类型T 相同。 |
function<T> f(nullptr); | 显式地构造一个空function |
function<T> f(obj) | 在f 中存储可调用对象obj 的副本 |
f | 将f 作为条件:当f 含有一个可调用对象时为真;否则为假。 |
定义为function<T> 的成员的类型 | |
result_type | 该function 类型的可调用对象返回的类型 |
argument_type | 当T 有一个或两个实参时定义的类型。如果T 只有一个实参,则argument_type |
first_argument_type | 第一个实参的类型 |
second_argument_type | 第二个实参的类型 |
-
例如:声明一个
function
类型,它可以表示接受两个int
,返回一个int
的可调用对象。function<int(int, int)>
重载、类型转换、运算符
类型转换运算符
-
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:
operator type() const;
-
一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是
const
。 -
避免过度使用类型转换函数。
-
C++11引入了显式的类型转换运算符。
-
向
bool
的类型转换通常用在条件部分,因此operator bool
一般定义成explicit
的。
避免有二义性的类型转换
-
通常,不要为类第几个亿相同的类型转换,也不要在类中定义两个及以上转换源或转换目标是算术类型的转换。
-
在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。
函数匹配与重载运算符
-
如果
a
是一种类型,则表达式a sym b
可能是:-
a.operatorsym(b);
-
operatorsym(a,b);
-
-
如果我们队同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
第十五章 面向对象程序设计
OOP:概述
-
面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定。
-
继承(inheritance):
-
通过继承联系在一起的类构成一种层次关系。
-
通常在层次关系的根部有一个基类(base class)。
-
其他类直接或者简介从基类继承而来,这些继承得到的类成为派生类(derived class)。
-
基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
-
对于某些函数,基类希望它的派生类个自定义适合自己的版本,此时基类就将这些函数声明成虚函数(virtual function)。
-
派生类必须通过使用类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:一个冒号,后面紧跟以逗号分隔的基类列表,每个基类前都可以有访问说明符。
class Bulk_quote : public Quote{};
-
派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上
virtual
关键字,也可以不加。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override
关键字。
-
-
动态绑定(dynamic binding,又称运行时绑定):
-
使用同一段代码可以分别处理基类和派生类的对象。
-
函数的运行版本由实参决定,即在运行时选择函数的版本。
-
定义基类和派生类
定义基类
-
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
-
基类通过在其成员函数的声明语句前加上关键字
virtual
使得该函数执行动态绑定。 -
如果成员函数没有被声明为虚函数,则解析过程发生在编译时而非运行时。
-
访问控制:
-
protected
: 基类和和其派生类还有友元可以访问。 -
private
: 只有基类本身和友元可以访问。
-
定义派生类
-
派生类必须通过类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:冒号,后面紧跟以逗号分隔的基类列表,每个基类前面可以有一下三种访问说明符的一个:
public
、protected
、private
。 -
C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个
override
关键字。 -
派生类构造函数:派生类必须使用基类的构造函数去初始化它的基类部分。
-
静态成员:如果基类定义了一个基类成员,则在整个继承体系中只存在该成员的唯一定义。
-
派生类的声明:声明中不包含它的派生列表。
-
C++11新标准提供了一种防止继承的方法,在类名后面跟一个关键字
final
。
类型转换与继承
-
理解基类和派生类之间的类型抓换是理解C++语言面向对象编程的关键所在。
-
可以将基类的指针或引用绑定到派生类对象上。
-
不存在从基类向派生类的隐式类型转换。
-
派生类向基类的自动类型转换只对指针或引用类型有效,对象之间不存在类型转换。
虚函数
-
使用虚函数可以执行动态绑定。
-
OOP的核心思想是多态性(polymorphism)。
-
当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
-
派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上
virtual
关键字,也可以不加。 -
C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个
override
关键字。 -
如果我们想覆盖某个虚函数,但不小心把形参列表弄错了,这个时候就不会覆盖基类中的虚函数。加上
override
可以明确程序员的意图,让编译器帮忙确认参数列表是否出错。 -
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
-
通常,只有成员函数(或友元)中的代码才需要使用作用域运算符(
::
)来回避虚函数的机制。
抽象基类
-
纯虚函数(pure virtual):清晰地告诉用户当前的函数是没有实际意义的。纯虚函数无需定义,只用在函数体的位置前书写
=0
就可以将一个虚函数说明为纯虚函数。 -
含有纯虚函数的类是抽象基类(abstract base class)。不能创建抽象基类的对象。
访问控制与继承
-
受保护的成员:
-
protected
说明符可以看做是public
和private
中的产物。 -
类似于私有成员,受保护的成员对类的用户来说是不可访问的。
-
类似于公有成员,受保护的成员对于派生类的成员和友元来说是可访问的。
-
派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
-
-
派生访问说明符:
-
对于派生类的成员(及友元)能否访问其直接积累的成员没什么影响。
-
派生访问说明符的目的是:控制派生类用户对于基类成员的访问权限。比如
struct Priv_Drev: private Base{}
意味着在派生类Priv_Drev
中,从Base
继承而来的部分都是private
的。
-
-
友元关系不能继承。
-
改变个别成员的可访问性:使用
using
。 -
默认情况下,使用
class
关键字定义的派生类是私有继承的;使用struct
关键字定义的派生类是公有继承的。
继承中的类作用域
-
每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
-
派生类的成员将隐藏同名的基类成员。
-
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
构造函数与拷贝控制
虚析构函数
-
基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
-
如果基类的析构函数不是虚函数,则
delete
一个指向派生类对象的基类指针将产生未定义的行为。 -
虚析构函数将阻止合成移动操作。
合成拷贝控制与继承
-
基类或派生类的合成拷贝控制成员的行为和其他合成的构造函数、赋值运算符或析构函数类似:他们对类本身的成员依次进行初始化、赋值或销毁的操作。
派生类的拷贝控制成员
-
当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
-
派生类析构函数:派生类析构函数先执行,然后执行基类的析构函数。
继承的构造函数
-
C++11新标准中,派生类可以重用其直接基类定义的构造函数。
-
如
using Disc_quote::Disc_quote;
,注明了要继承Disc_quote
的构造函数。
容器与继承
-
当我们使用容器存放继承体系中的对象时,通常必须采用间接存储的方式。
-
派生类对象直接赋值给积累对象,其中的派生类部分会被切掉。
-
在容器中放置(智能)指针而非对象。
-
对于C++面向对象的编程来说,一个悖论是我们无法直接使用对象进行面向对象编程。相反,我们必须使用指针和引用。因为指针会增加程序的复杂性,所以经常定义一些辅助的类来处理这些复杂的情况。
文本查询程序再探
-
使系统支持:单词查询、逻辑非查询、逻辑或查询、逻辑与查询。
面向对象的解决方案
-
将几种不同的查询建模成相互独立的类,这些类共享一个公共基类:
-
WordQuery
-
NotQuery
-
OrQuery
-
AndQuery
-
-
这些类包含两个操作:
-
eval
:接受一个TextQuery
对象并返回一个QueryResult
。 -
rep
:返回基础查询的string
表示形式。
-
-
继承和组合:
-
当我们令一个类公有地继承另一个类时,派生类应当反映与基类的“是一种(Is A)”的关系。
-
类型之间另一种常见的关系是“有一个(Has A)”的关系。
-
-
对于面向对象编程的新手来说,想要理解一个程序,最困难的部分往往是理解程序的设计思路。一旦掌握了设计思路,接下来的实现也就水到渠成了。
Query程序设计:
操作 | 解释 |
---|---|
Query 程序接口类和操作 | |
TextQuery | 该类读入给定的文件并构建一个查找图。包含一个query 操作,它接受一个string 实参,返回一个QueryResult 对象;该QueryResult 对象表示string 出现的行。 |
QueryResult | 该类保存一个query 操作的结果。 |
Query | 是一个接口类,指向Query_base 派生类的对象。 |
Query q(s) | 将Query 对象q 绑定到一个存放着string s 的新WordQuery 对象上。 |
q1 & q2 | 返回一个Query 对象,该Query 绑定到一个存放q1 和q2 的新AndQuery 对象上。 |
q1 | q2 | 返回一个Query 对象,该Query 绑定到一个存放q1 和q2 的新OrQuery 对象上。 |
~q | 返回一个Query 对象,该Query 绑定到一个存放q 的新NotQuery 对象上。 |
Query 程序实现类 | |
Query_base | 查询类的抽象基类 |
WordQuery | Query_base 的派生类,用于查找一个给定的单词 |
NotQuery | Query_base 的派生类,用于查找一个给定的单词 |
BinaryQuery | Query_base 的派生类,查询结果是Query 运算对象没有出现的行的集合 |
OrQuery | Query_base 的派生类,返回它的两个运算对象分别出现的行的并集 |
AndQuery | Query_base 的派生类,返回它的两个运算对象分别出现的行的交集 |
第十六章 模板和泛型编程
-
面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。
-
OOP能处理类型在程序运行之前都未知的情况;
-
泛型编程中,在编译时就可以获知类型。
-
定义模板
-
模板:模板是泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。
函数模板
-
template <typename T> int compare(const T &v1, const T &v2){}
-
模板定义以关键字
template
开始,后接模板形参表,模板形参表是用尖括号<>
括住的一个或多个模板形参的列表,用逗号分隔,不能为空。 -
使用模板时,我们显式或隐式地指定模板实参,将其绑定到模板参数上。
-
模板类型参数:类型参数前必须使用关键字
class
或者typename
,这两个关键字含义相同,可以互换使用。旧的程序只能使用class
。 -
非类型模板参数:表示一个值而非一个类型。实参必须是常量表达式。
template <class T, size_t N> void array_init(T (&parm)[N]){}
-
内联函数模板:
template <typename T> inline T min(const T&, const T&);
-
模板程序应该尽量减少对实参类型的要求。
-
函数模板和类模板成员函数的定义通常放在头文件中。
类模板
-
类模板用于生成类的蓝图。
-
不同于函数模板,编译器不能推断模板参数类型。
-
定义类模板:
-
template <class Type> class Queue {};
-
-
实例化类模板:提供显式模板实参列表,来实例化出特定的类。
-
一个类模板中所有的实例都形成一个独立的类。
-
模板形参作用域:模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
-
类模板的成员函数:
-
template <typename T> ret-type Blob::member-name(parm-list)
-
-
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
-
新标准允许模板将自己的类型参数成为友元。
template <typename T> class Bar{friend T;};
。 -
模板类型别名:因为模板不是一个类型,因此无法定义一个
typedef
引用一个模板,但是新标准允许我们为类模板定义一个类型别名:template<typename T> using twin = pair<T, T>;
模板参数
-
模板参数与作用域:一个模板参数名的可用范围是在声明之后,至模板声明或定义结束前。
-
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置。
-
当我们希望通知编译器一个名字表示类型时,必须使用关键字
typename
,而不能使用class
。 -
默认模板实参:
template <class T = int> class Numbers{}
成员模板
-
成员模板(member template):本身是模板的函数成员。
-
普通(非模板)类的成员模板。
-
类模板的成员模板。
-
控制实例化
-
动机:在多个文件中实例化相同模板的额外开销可能非常严重。
-
显式实例化:
-
extern template declaration; // 实例化声明
-
template declaration; // 实例化定义
-
效率与灵活性
模板实参推断
-
对函数模板,编译器利用调用中的函数实参来确定其模板参数,这个过程叫模板实参推断。
类型转换与模板类型参数
-
能够自动转换类型的只有:
-
和其他函数一样,顶层
const
会被忽略。 -
数组实参或函数实参转换为指针。
-
函数模板显式实参
-
某些情况下,编译器无法推断出模板实参的类型。
-
定义:
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
-
使用函数显式实参调用:
auto val3 = sum<long long>(i, lng); // T1是显式指定,T2和T3都是从函数实参类型推断而来
-
注意:正常类型转换可以应用于显式指定的实参。
尾置返回类型与类型转换
-
使用场景:并不清楚返回结果的准确类型,但知道所需类型是和参数相关的。
-
template <typename It> auto fcn(It beg, It end) -> decltype(*beg)
-
尾置返回允许我们在参数列表之后声明返回类型。
标准库的类型转换模板:
-
定义在头文件
type_traits
中。
对Mod<T> ,其中Mod 是: | 若T 是: | 则Mod<T>::type 是: |
---|---|---|
remove_reference | X& 或X&& | X |
否则 | T | |
add_const | X& 或const X 或函数 | T |
否则 | const T | |
add_lvalue_reference | X& | T |
X&& | X& | |
否则 | T& | |
add_rvalue_reference | X& 或X&& | T |
否则 | T&& | |
remove_pointer | X* | X |
否则 | T | |
add_pointer | X& 或X&& | X* |
否则 | T* | |
make_signed | unsigned X | X |
否则 | T | |
make_unsigned | 带符号类型 | unsigned X |
否则 | T | |
remove_extent | X[n] | X |
否则 | T | |
remove_all_extents | X[n1][n2]... | X |
否则 | T |
函数指针和实参推断
-
当使用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
模板实参推断和引用
-
从左值引用函数推断类型:若形如
T&
,则只能传递给它一个左值。但如果是const T&
,则可以接受一个右值。 -
从右值引用函数推断类型:若形如
T&&
,则只能传递给它一个右值。 -
引用折叠和右值引用参数:
-
规则1:当我们将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如
T&&
),编译器会推断模板类型参数为实参的左值引用类型。 -
规则2:如果我们间接创造一个引用的引用,则这些引用形成了折叠。折叠引用只能应用在间接创造的引用的引用,如类型别名或模板参数。对于一个给定类型
X
:-
X& &
、X& &&
和X&& &
都折叠成类型X&
。 -
类型
X&& &&
折叠成X&&
。
-
-
上面两个例外规则导致两个重要结果:
-
1.如果一个函数参数是一个指向模板类型参数的右值引用(如
T&&
),则它可以被绑定到一个左值上; -
2.如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数(
T&
)。
-
-
理解std::move
-
标准库
move
函数是使用右值引用的模板的一个很好的例子。 -
从一个左值
static_cast
到一个右值引用是允许的。
template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }
转发
-
使用一个名为
forward
的新标准库设施来传递参数,它能够保持原始实参的类型。 -
定义在头文件
utility
中。 -
必须通过显式模板实参来调用。
-
forward
返回显式实参类型的右值引用。即,forward<T>
的返回类型是T&&
。
重载与模板
-
多个可行模板:当有多个重载模板对一个调用提供同样好的匹配时,会选择最特例化的版本。
-
非模板和模板重载:对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
可变参数模板
可变参数模板就是一个接受可变数目参数的模板函数或模板类。
-
可变数目的参数被称为参数包。
-
模板参数包:标识另个或多个模板参数。
-
函数参数包:标识另个或者多个函数参数。
-
-
用一个省略号来指出一个模板参数或函数参数,表示一个包。
-
template <typename T, typename... Args>
,Args
第一个模板参数包。 -
void foo(const T &t, const Args& ... rest);
,rest
是一个函数参数包。 -
sizeof...
运算符,返回参数的数目。
编写可变参数函数模板
-
可变参数函数通常是递归的:第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
包扩展
-
对于一个参数包,除了获取它的大小,唯一能做的事情就是扩展(expand)。
-
扩展一个包时,还要提供用于每个扩展元素的模式(pattern)。
转发参数包
-
新标准下可以组合使用可变参数模板和
forward
机制,实现将实参不变地传递给其他函数。
模板特例化(Specializations)
-
定义函数模板特例化:关键字
template
后面跟一个空尖括号对(<>
)。 -
特例化的本质是实例化一个模板,而不是重载它。特例化不影响函数匹配。
-
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是特例化版本。
-
我们可以部分特例化类模板,但不能部分特例化函数模板。