模板面试知识点
- 模板特化和偏特化[2]。
- 函数模板和类模板的定义通常需要放在头文件中。为了生成一个实例化版本,编译器需要掌握函数模板或者类模板成员函数的定义[3]。
- 函数重载和模板特化的关系。模板特化不影响函数匹配,实际上,我们是替编译器实现了重载之后的某个特殊类型的实例化。
一些函数
remove_reference
,可以将一个引用类型(包括左值引用和右值引用)变成非引用类型,通过类模板特化完成。std::move
,将一个左值转换成右值,或者保持右值不变。通过static_cast完成。std::forward<T>(arg)
。1
2
3
4
5template<typename Type>
void func(Type && arg)
{
another_func(std::forward<Type>(arg));
}
上述代码会做到将func的实参原封不动的(保持所有属性)转发到another_func中,具体的过程:首先根据引用折叠和右值引用参数推断出Type的类型,如果arg是右值,Type是普通类型,如果arg是左值,则Type是引用的右值,通过引用折叠,得到Type是一个左值引用。推断出Type的类型之后,std::forward
模板定义
模板参数列表
模板定义以关键字template开始,后跟一个模板参数列表,是一个逗号分隔的一个或者多个模板参数的列表,用<
和>
分隔开来。模板参数可以是模板类型参数和非类型模板参数。
模板类型参数
一个模板类型参数表示一个类型,类型参数前需要加上class
或者typename
关键字,它们是等价的。
非类型模板参数
而非类型模板参数是一个值,而不是一个类型,通过一个特定的类型名指定非类型参数。
模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但是没有指出如何初始化它们。运行时,需要调用者提供实参来初始化形参。而模板参数表示在类或者函数定义中用到的类型或者值。使用模板时,我们需要显式或者隐式的指定模板实参,将它绑定到模板参数上。
模板种类
函数模板
调用函数模板时,编译器会用函数实参推断模板实参。
类模板
编译器不能为类模板推断模板类型参数,为了使用类模板,必须在模板名后的尖括号提供模板实参列表代替模板参数。
- 实例化类模板。
使用类模板时,需要提供显式模板实参列表,编译器使用这些模板实参进行实例化。 - 类模板的成员函数。
定义在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表。在类外定义一个成员时,不仅要说明成员属于哪个类,从一个模板生成的类的名字中必须包含它的模板实参(比如Blob<T>
)。 - 类模板成员函数的实例化。
对于一个实例化了的类模板,它的成员只有在使用时才被实例化,所以即使某种类型不完全符合模板操作的类型,也能用这个类型实例化类。 - 在类的作用域内简化模板类名的使用。
在类模板自己的作用域内,不需要使用Blob<T>
,使用Blob
即可。 - 类模板和友元。
仅仅每个实例类型之间互为友元。(template_friend_1.cpp)
一个类可以将另一个模板的所有实例都声明为友元。(template_friend_2.cpp)
一个类可以是一个类模板所有实例的友元。(template_friend_3.cpp)
一个类模板将另一个类模板的所有实例都声明为友元。(template_friend_4.cpp)
一个模板将模板类型参数声明为友元。 - 模板类型别名。
使用using可以为类模板定义一个别名。 - 类模板的static成员。
每个实例都有一个自己的static成员。
模板参数
- 模板参数和作用域。
模板内的变量名不能和模板参数名冲突。 - 使用类的类型成员。
默认情况下,假定通过作用域运算符访问的是名字不是类型。如果需要访问类型的话,必须使用typename关键字告诉编译器这是一个类型。 - 默认模板实参。
C++ 11之后,可以为函数和类模板都提供默认实参。在使用类模板的时候,必须在模板名后加上尖括号,如果一个类模板的所有模板参数都有默认实参,而且想要使用默认实参,需要使用`<>。
成员模板
一个类(普通类或者类模板)都可以包含本身是模板的函数,这种成员被称为成员模板,成员模板不能是虚函数。
- 普通类的成员模板。
- 类模板的成员模板。
类模板和成员模板有各自的模板参数。当在类模板外定义成员模板的时候,必须同时为类模板和成员模板提供模板参数列表,类模板的参数列表在前,成员模板的参数列表在后。
实例化定义
模板在使用时才被实例化,当多个独立编译的源文件同时使用了相同的模板和模板参数列表时,可能而存在多个同一模板的示例,使用显式实例化避免这种开销。1
2
3
4
5extern template declaration; //实例化声明
template declaration; //实例化定义
// 示例
extern template Blob<string>;
template Blob<string>;
实例化定义会实例化所有的成员,所以用来实例化的类型,必须能用于模板的所有成员。
效率和灵活性
shared_ptr
在运行时绑定deleter,deleter不是shared_ptr
的成员。
unique_ptr
在编译是绑定deleter,deleter是unique_ptr
的一个成员。
模板实参推断
类型转换和模板类型参数
- 能用于函数模板的类型转化。
const
转换,可以将非顶层const 转换为顶层const- 数组或者函数转换为相应的指针。
- 相同模板类型参数的函数形参。
- 不同模板类型参数的函数形参。
- 正常类型转换对应于普通函数实参。
函数模板显式实参
对于一些编译器无法推断出的模板实参类型,可以指定显式模板实参。而且显式模板实参的顺序和模板参数声明的顺序一致。
尾置返回类型
可以使用尾置返回类型声明不知道返回结果的类型。
函数指针的实参推断
当参数是一个函数模板实例的地址时,程序的上下文必须满足,对于每个模板参数,能唯一确定它的类型或者值。
模板实参推断和引用
- 当函数参数是一个普通左值引用的时候,只能给他传递一个左值,实参可以是const,也可以不是。如果实参是const的,T会被推断成const类型。
当函数参数是一个常量引用的时候,实参可以是任何对象。这个const是函数参数类型的一部分,而不是模板参数类型的一部分。 - 也可以推断出右值实参的类型。
- 引用折叠和右值引用参数。如果一个函数参数是指向模板参数类型的右值引用,比如T&& val,可以给val传递任意类型的实参(左值或者右值)。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。
理解std::move
std::move的源代码如下:1
2
3
4
5template<typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
所以,从代码中我们可以看出来,是可以显式的将一个左值static_cast到一个右值引用。
forward
forward指的是将实参的所有性质原封不动的转发给其他函数,包括实参类型是否是const的以及实参是左值还是右值。
**forward1
2
3
4
5
6
7
8
9template<Typename T>
void func(Type && arg)
{
// 首先对右值引用模板参数进行推导,如果arg是右值,那么推断出Type是普通非引用类型。如果arg是左值,推断出Type是左值引用类型。
std::forward<Type>(arg);
// 调用std::forward获得Type的右值引用,通过引用折叠,可以得到相应的左值和右值类型变量。
}
重载与模板
函数模板匹配规则,如果有多个函数都满足要求,选择其中最特殊的那个:普通函数比函数模板特殊,都是函数模板的时候,选择更特殊的那个,否则就有歧义。
可变参数模板
可变参数模板是一个接收可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包和函数参数包,模板参数包表示零个或者多个模板参数,就是typename加上… ,函数参数包表示零个或者多个函数参数,是模板类型加上…。
模板特化
函数模板特化和类模板特化。特化一个函数模板时,必须为原函数模板中的每个模板参数都提供实参,叫做全特化。类模板的特化不需要给所有的模板参数提供实参,可以指定一部分参数而不是所有参数,即偏特化。
特化的本质是实例化,和模板的重载本质上是不同的,特化不会影响函数匹配。
参考文献
1.《C++ Primer》第五版中文版
2.https://stackoverflow.com/questions/8061456/c-function-template-partial-specialization
3.https://isocpp.org/wiki/faq/templates