1.定义模板
? 模版是C++泛型编程的基础,一个模版就是一个类或函数的蓝图或者说是公式:例如在使用vector这样的泛型类型,或者是find函数这样的泛型类型,我们可以将蓝图转换为特定的类或者是函数,这种转换发生在编译时。当我们调用一个模板时,编译器会使用实参的类型来确定绑定到模版参数T上的类型,之后编译器利用推断出的模版参数来实例化一个特定版本的函数,这个过程被称之为实例化。编译器遇到一个模版的定义时,并不会产生代码,只有当我们实例化出模版的一个特定版本的时候,编译器才会产生代码,因此有很多bug等问题要等用到(实例化)的时候才会发现。
1.1 函数模板
-
类型参数之前必须加上 typename 或者 class 这两个关键字,这两个关键字含义相同,可以互换使用。但有些地方只有 typename 才可以用,比如当我们表示一个容器类型时,只能使用 typename 关键字。 -
模板类型参数包括类型参数和非类型模板参数。类型参数很好理解,不多赘述。下面讲解非类型模板参数。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字 class 或者 typename 来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
示例如下: template <unsigned N, unsigned M> //N和M分别表示数组的长度
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
当我们调用这个版本的compare时:compare(“hi”, “mom”); 编译器使用字面常量的大小来代替N和M,从而实例化模板。 一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期(参见第12章,第400页)。我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。 -
inline 和 constexpr 这两个说明符的位置和普通的非模板函数一样。 -
与非模板代码不同,函数模板和类模板成员函数的定义通常放在头文件中,即模板的头文件中通常既包括声明也包括定义。
1.2 类模板
-
为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——显示模板实参,它们用来代替模板参数的模板实参列表。 -
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍能使用该类型实例化类。 -
我们可以在类模版的内部或者外部对类模版的成员函数进行定义,定义在类模版内的成员函数被隐式的声明为inline函数。 -
在类模版自己的作用域中(即类内),我们可以直接使用模版名而不提供实参(不需要这个东西了),而在类外则需要指定模版参数。 -
当一个类模版包含一个非模版友元,则友元被授权可以访问所有的模版实例,如果友元自身是模版,类可以授权给所有友元模版实例,也可以只授予给定实例。如果想要所有实例成为友元,友元声明中必须使用与类模版不同的模版参数。注意这里的前置声明问题。(参见589页) -
C++11新标准:可以将模版参数类型声明为友元,比如将 Sales_data 类将称为 Bar<Sales_data> 的友元。
虽然友元通常来说应该是一个类或是一个函数,但我们完全可以用一个内置类型来实例化Bar。这种与内置类型的友好关系是允许的,以便我们能用内置类型来实例化Bar这样的类。
-
C++11新标准:我们可以定义一个typedef来引用实例化的类,还可以使用using来声明类型别名。 typedef Blob<string> StrBlob; //引用了实例化的类
template <typename T> using twin = pair<T, T> //起了一个别名
template <typename T> using patrNo = pair<T, unsigned>;
partNo<string> books; //books是一个pair<string, unsigned>
partNo<Student> kids; //kids是一个pair<Student, unsigned>
1.3 模板参数
- 在类模板中,默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果希望使用一个模板类型参数的类型成员,就必须显示告诉编译器该名字是一个类型。我们通过使用关键字typename来实现这一点。例如如下代码:
template <typename T>
typename T::value_type top(const T& c)
{
if(!c.empty())
return c.back();
else
return typename T::value_type();
}
? 例如该函数的返回值以及第七行,通过typename指定返回值为T类型的value_type。此处只能使用 typename, 不能使用 class ,这是它们的一个区别。
- C++11新标准允许提供默认模版实参。(详情参见594页)
1.4 成员模板
-
一个类无论是类模版还是普通类,都可以将其成员函数定义为模版,称之为成员模版,但是成员模版不能是虚函数。 -
与类模板的普通函数成员不同,成员模板是函数模板。当我**们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。**在类模版内定义模版函数,主要的区别就是他们有着自己独立的模版参数,所以我们在类模版外定义一个模版成员函数时,必须同时为类模版和成员模版提供模版参数。 template <typename T> class Blob{
template <typename It> Blob(It begin, It end);
...
}
//这里是类外
template <typename T>
template <typename It>
Blob<T>::Blob(It begin, It end) : data(make_shared<vector<T>>(begin, end)){}
1.5 控制实例化
? 对于模版使用时才会被实例化,会产生一个问题:相同的实例可能会出现在多个对象文件中,这时候每个文件都会有一份实例化的副本,在大系统中,这无疑造成了很大的额外开销,所以在C++11新标准下,我们可以使用显示实例化以避免这样的开销。
-
当编译器遇到 extern 模版声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern,就表示承诺在程序其他位置有该实例化的一个非extern定义且只能有一个定义。 extern template declaration; //实例化声明 template declaration; //实例化定义 extern template class Blob<string>; //声明
template int compare(const int&, const int&); //定义
-
extern 声明必须出现在任何使用该实例化版本的代码之前,其定义可以不放在本文件中,必须出现在程序的其他文件中。 -
**一个类模版的实例化定义会实例化该模版的所有成员,包括内联函数成员,**因为我们也不知道程序会使用那些成员函数,所以我们必须将其全部实例化,这就要求在实例化定义中,所用类型必须能用于模板的所有成员。
1.6 效率与灵活性
- 标准库智能指针类型,shared_ptr可以共享指针,unique_ptr独享指针,他们允许用户重载其删除器的方式。
- unique_ptr在编译时绑定删除器,shared_ptr在运行是绑定编译器。
2.模板实参推断
2.1 类型转换与模板类型参数
? 从函数实参到模版实参的过程被称为模版实参推断,在这个过程中,可能会发生类型转换。
- 与往常一样,顶层 const 无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项。
-
const 转换: 可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。 -
数组或函数指针转换: 如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。 其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。 将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有 const 转换及数组或函数到指针的转换。
-
应使用相同模板参数类型的函数形参。例如: template <typename T>
int compare(const T &r1, const T &r2)
{
...//比较操作
}
//以下是在main函数中
int a = 3;
long b = 6;
compare(a, b); //调用错误,推断出的类型不匹配。
为了解决上述问题,我们可以将函数模板定义为两个类型参数。 template <typename T, typename V>
int compare(const T &r1, const V &r2)
-
**正常的类型转换应用于普通函数实参。**函数模版也可以有用普通类型定义的参数,不使用 T 而使用 int等等,这些参数可以进行正常的类型转换。
2.2 函数模板显示实参
? 在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
-
我们可以指定显示模板实参。我们可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型: //编译器无法推断T1,它未出现在函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
? 在本例中,没有任何函数实参的类型可用来推断T1的类型。每次调用sum时调用者都必须为T1提供一个显式模板实参。我们提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号中给出,位于函数名之后,实参列表之前。 ? 注意,函数模版实参是按从左至右顺序进行匹配的,第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,只有尾部(最右)的参数可以忽略(忽略的前提是可以通过传入的实参进行推断),所以不要将最后的参数作为返回类型,否则需要将所有的模版实参进行显式的初始化。 -
**正常类型转换可以应用于显示指定的实参。**考虑之前的compare函数。 long lng;
compare(lng, 1024); //错误:模板参数不匹配
compare<long>(lng, 1024); //正确:实例化compare(long, long)
compare<int>(lng, 1024); //正确:实例化compare(int, int)
2.3 尾置返回类型与类型转换
? 1. 当我们想指定函数的返回类型时,使用显式模版实参是非常有效的,但是可能会给用户带来额外的负担,而且可能还会带来错误,比如返回值若是序列中的一个元素(接收的参数是序列的迭代器),我们可以使用尾置返回类型。
//尾置返回允许我们在参数列表之后声明返回类型 template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
...
return *beg; //返回序列中一个元素的引用
}
-
为了获得元素类型,我们可以使用标准库的类型转换模板。这些模板定义在头文件type_traits中。 *remove_reference<decltype(beg)>::type 它脱去了decltype(*beg)返回值的引用类型。(更多内容见606页)
2.4 函数指针和实参推断
-
若我们使用一个函数模版初始化一个函数指针或为一个函数指针赋值,编译器会使用指针的类型来推断模版的实参。 int(*p)(const int&, const int&) = compare;//compare中的T类型会根据指针的类型决定,在这里变为int
- 当一个参数为函数模版的实例的地址时,程序上下文必须满足:对每个模版实参都能唯一确定其类型或值。
void func(int (*)(const string&, const string&));
void func(int (*)(const int&, const int&));
func(compare); //error:使用compare的哪个实例?
func(compare<int>); //正确,显示指定出实例化哪个compare版本
2.5 模板实参推断和引用
-
当模版参数类型是一个左值引用时(T&),只能传递给它一个左值(一个变量、一个返回引用类型的表达式),实参可以是const类型,如果实参是const,T 将会被推断为 const 类型。 -
当模版参数类型是const T&时,我们可以传递给它任何类型的实参(一个非const或const的对象、一个临时对象、一个字面常量值),由于参数本身是const,T的推断类型将不会是const类型,const已经是函数参数类型的一部分,所以const将不会出现在 T 中。 -
当函数参数为右值引用时(T&&),正常的绑定规则是只能传递右值的,但是却又两个例外允许将左值也绑定到右值引用上。
- 第一个例外:当我们将一个左值传递给右值引用时,编译器会推断模版参数类型为实参的左值引用类型,即将 T 推断为 T&,以使用左值引用(一般情况下我们不能定义一个引用的引用,但是通过类型别名或者模版参数类型间接定义是可以的)
- 第二个例外:==如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。==在新标准中,折叠规则扩展到右值引用。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即,对于一个给定类型 x:
- x& &、X& &&和X&& &都折叠成类型 x&
- 类型x&& &&折叠成X&&
如果一个函数参数是指向模板参数类型的右值引用(如, T &&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。
(以上内容参见 C++ primer 609页有关引用折叠的知识。)
? 在实际中,右值引用通常用于两种情况:模板转发其实参 或 模板被重载
2.6 理解std::move
? 我们可以用static_cast显示地将一个左值转换为一个右值引用。
? 标准库是这样定义move的:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
2.7 转发
? **转发:某些函数需要将其一个或者多个实参连同类型和性质都不变的转发给其他函数。**我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。
-
由引用折叠规则可知,如果一个函数参数是指向模板类型参数的右值引用(如 T &&),它对应的实参的 const 属性和左值 / 右值属性将得到保持。 -
使用 std::forward 保持类型信息,forward()会保持实参类型的每个细节,头文件为 utility,使用时其后必须加显式模版参数,forward返回该实参类型的右值引用。即,forward的返回类型是 T &&。(通过引用折叠规则即可理解) 如果以后忘了forward()为什么会对左值有效时,就看下面↓
如果实参是一个右值,则Type是一个普通(非引用)类型,forward 将返回 Type&&。如果实参是一个左值,则通过引用折叠,Type 本身是一个左值引用类型。在此情况下,返回类型是一个指向左值引用类型的右值引用。再次对 forward 的返回类型进行引用折叠,将返回一个左值引用类型。
3. 重载与模板
? **函数模版可以被另一个模版或者普通非模版函数重载。**即相同函数之间具有不同数量或者类型的参数。
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
- 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序,需要类型转换的排在不需要转换的后面。当然,可以用于函数模板调用的类型转换是非常有限的。
- 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数但是,如果有多个函数提供同样好的匹配,则:
- 如果同样好的函数中只有一个是非模板函数,则选择此函数。
- 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
- 否则,此调用有歧义。
4. 可变模板参数
? 可变函数模版就是指一个接受可变数目参数的模版函数或者模版类,可变数目的参数被称为参数包,分为两种:模版参数包,表示零个或多个模版参数,函数参数包,表示零个或多个函数参数。我们用一个省略号来表示一个模版参数或者函数参数为一个包,在一个模板参数列表中,class… 或 typename… 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。使用参数初始化列表 initializer_list 也可以定义一个可接受可变参数数目的函数,但是所有的实参都必须具有相同的类型(或者可转变为相同的类型)。
// Args是一个模板参数包;rest是一个函数参数包
// Args表示零个或多个模板类型参数
// rest表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T &t, const Args&... rest);
当我们需要知道包中有多少个元素时,可以使用 sizeof… 运算符。注意这里,它不是 **sizeof() **运算符! 类似 sizeof ,sizeof… 也返回一个常量表达式,而且不会对其实参求值。
template<typename ... Args> void g(Args ... args)
{
cout << sizeof...(Args) << endl; //类型参数的数目
cout << sizeof...(args) << endl; //函数参数的数目
)
以上两个输出会输出相同的大小,即包的大小。即使你类型数目有2个,参数数目有10个,两个 sizeof… 输出的也都是10,它只能用来求包的大小。
4.1 编写可变参数函数模板
- 可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
//用来终止递归并打印最后一个元素的函数
//此函数必须在可变参数版本的 print 定义之前声明 template<typename T>
ostream& print (ostream &os, const T &t)
{
return os << t; //包中最后一个元素之后不打印分隔符
}
//包中除了最后一个元素之外的其他元素都会调用这个版本的print
template <typename T, typename... Args>
ostream& print (ostream &os, const T &t, const Args&... rest) //扩展Args
{
os << t << ","; //打印第一个实参
return print (os, rest...); //递归调用,打印其他实参;扩展rest
}
//main()中
int i;
double s;
print(cout, i, s, 42);
4.2 包扩展
-
对于一个参数包,我们还可以对其进行参数扩展,即将一个包分解为其构成元素,我们通过在模式的右边放一个省略号…来触发扩展操作。模式是函数参数包的名字。 -
print 中的函数参数包扩展仅仅将包扩展为其构成元素,C++语言还允许更复杂的扩展模式。例如,我们可以编写第二个可变参数函数,对其每个实参调用 debug_rep (参见16.3节,第615页),然后调用 print打印结果string: //在print调用中对每个实参调用debug_rep
template <typename... Args>
ostream& errorMsg(ostream &os, const Args&... rest)
{
// print (os, debug_rep(al), debug_rep(a2), ..., debug_rep(an)
return print(os, debug_rep(rest)... );
}
扩展中的模式会独立地应用于包中的每个元素。
4.3 转发参数包
? 在新标准下,我们可以组合使用可变参数模板与 forward 机制来编写函数,实现将其实参不变地传递给其它函数。下面举出一个例子,(具体例子看623页)
template <class... Args>
inline void strVec::emplace_back(Args&&... args)
{
chk_n_alloc(); //如果需要的话重新分配StrVec内存空间
alloc.construct(first_free++, std::forward<Args>(args)...);
}
5.模板特例化
? 当我们不能(或者不希望)使用模版版本时,我们可以定义类模版或者函数模版的一个特例化版本:比如说函数模版中的处理不适用于未定义 < 运算符(指针类型)的情况,我们就可以特例化一个版本以使用特殊情况。一个特例化版本就是模版的一个独立的定义,在其中一个或者多个参数被指定为特定的类型。特例化的本质是实例化一个模版,而非重载,因此特例化不影响函数的重载,它不是一个非模版的独立函数。
比如之前的 compare 函数就是个很好的例子,具体的说明见625页。
- 在为**函数模版**特例化时,必须为函数模版的每个模版参数提供实参。为了指出我们正在实例化一个模板,应使用关键字 template 后跟一个空尖括号对 <> 。空尖括号指出我们将为原模板的所有模板参数提供实参:
template <>
int compare(const char* const &p1, const char* const &p2)
{
return strcmp(p1, p2);
}
? 模版及其特例化版本应该定义在一个头文件中,所有同名的模版的声明应该放在前面,然后是这些特例化的声明。
-
类模版也可以进行特例化,我们需要在原模版定义所在的命名空间中特例化,本节用例:hash模版所在命名空间为标准库std,所以我们可以打开标准空间对该命名空间添加成员。 namespace std{ //打开命名空间
template <>
struct hash<Sales_data>
{
... //有关定义
}
} //关闭命名空间
-
关于hash类:默认情况下,无序容器使用 hash<key_type> (参见394页)来组织其元素。 一个特例化 hash 类必须定义:
- 一个重载的调用运算符(参见14.8节,第506页),它接受一个容器关键字类型的对象,返回一个size_t。
- 两个类型成员,result _type 和 argument_type,分别调用运算符的返回类型和参数类型。
- 默认构造函数和拷贝赋值运算符(可以隐式定义,参见13.1.2节,第443页)。
-
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。
关于标准库 remove_reference 类型,该模板是通过一系列的特例化版本来完成其功能的。 //原始的、最通用的版本
template <class T> struct remove_reference {
typedef T type;
};
//部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference <T&> //左值引用
{ typedef T type; };
template <class T> struct remove_reference <T&&>//右值引用
{ typedef T type; };
-
我们还可以只特例化成员函数而不是特例化整个模板。例如,如果Foo是一个模板类,包含一个成员Bar, 我们可以只特例化该成员。 template <typename T> struct Foo
{
Foo (const T &t = T()) : mem(t) {}
void Bar() {/* ...*/ }
T mem;
// Foo的其他成员
};
template<> //我们正在特例化一个模板
void Foo<int>::Bar() //我们正在特例化Foo<int>的成员Bar
{
//进行应用于int的特例化处理
}
//main函数中
Foo<string> fs; //实例化Foo<string>::Foo()
fs.Bar() ; //实例化Foo<string>::Bar()
Foo<int> fi; //实例化Foo<int>::Foo()
fi.Bar(); //使用我们特例化版本的Foo<int>::Bar()
本例中我们只特例化 Foo 类的一个成员,其他成员将由Foo模板提供。 当我们用 int 之外的任何类型使用 Foo 时,其成员像往常一样进行实例化。当我们用 in t使用 Foo 时,Bar 之外的成员像往常一样进行实例化。如果我们使用 Foo 的成员 Bar,则会使用我们定义的特例化版本。
本博客为自己最近学习 C++ 模板时整理的笔记,供个人学习使用,如果错误欢迎指正。参考书籍为《C++ primer 第五版》,笔记内容除了自己的总结外,还借鉴了很多其他博主的笔记。
|