引言与概述
C++模板机制允许在定义类,函数,类型别名的时候将类型或值当作参数,这样定义的类和函数在运行时间和空间效率上并不逊色于手工打造的非通用的代码。
模板提供的代码是类型安全的。
模板是一种编译时期的机制,与手工编写的代码相比,并不会产生任何运行期开销。
对模板来说,只有当一个成员函数被使用时才会被生成代码。
一个通用的组件应该从一个或多个具体实例泛化而来,而不是简单的从第一原理直接而来。 即我们可以先写一个具体的函数或者类,然后改成泛化类型。
模板简介
一个简单的模板
template <typename C>
class String {
public:
String();
explicit String(const C* c);
private:
static const int short_max = 15;
int sz;
C* ptr;
};
一个模板类的声明与普通class的声明差别不大,只需要添加关键字和将需要泛化的类型进行替换。
模板成员函数也可以不用定义在类内,也可以在外部定义,但模板成员函数本身还是一个模板,所以要显式声明一个模板:
template<typename C>
String<C>::String():
sz(0),ptr(nullptr)
{}
模板实例化
模板实例化:从一个模板和一个模板实参列表生成一个类或者一个函数的过程。
{
String<char> str;
}
即可以进行变量的声明,注意这里我们只创建了对象,即这个类型String< char > 编译器会为其生成默认构造函数和析构函数,不使用的成员对象或者函数则不会生成。
模板提供了一种用少量代码来生成大量代码的机制,但我们要小心实例代码的泛滥,造成内存的大量占用。 通过组合模板和简单内联可以消除很多直接或者间接函数调用的开销。
模板参数
模板机制的最大弱点是无法直接表达对模板参数的要求。
template <Container Cont, typename Elem>
requires Equal_comparable<Cont::value_type, Elem>()
int find_index(Cont& c, Elem e);
但我们可以这写来进行模板参数的检查。 在C++20 出现了concept这种技术,可以帮助我们进行对模板参数要求的检查。
但这种检查是在编译过程中非常晚的时刻进行的,而且是在抽象层次很低的层次上运行的,帮助有限。
成员类型别名
如果我们想要在类外使用模板参数,目前只有使用成员类型别名这种方法:
template <typename C>
class String {
public:
using value_type = T;
};
static成员
一个static的成员只有被真正使用时才被定义。
template <typename T>
struct X
{
static int a;
static int b;
};
int* p = &X<int>::a;
如果这就是全部代码,那么 a 会被报错为无定意,而b就不会。
模板与virtual
模板成员函数不能是虚函数,如果使用那么为虚函数实现的传统技巧虚表就很难使用,并且链接器的复杂性也会很高。
模板与嵌入类型
在模板中尽量避免嵌入类型,除非它们真正依赖所有的模板参数。
模板与友元
template <typename T>
class B;
template <typename T>
void A(const B<T>& b);
template <typename T>
class B {
public:
friend void A<>(const B<T>& b);
};
友元后面的<>是必须的,它指明了友元是一个模板函数,如果没有<>,则友元函数被假定为非模板函数。友元函数只有被使用时才会被实例化。
友元的设计目的是为了表达一小群紧密相关的概念,如果友元关系很复杂,那么一定是一个设计错误。
源码组织
使用模板组织源码有三种很明显的方法:
- 在一个编译单元中,在使用模板前包含其定义;
- 在一个编译单元中,在使用模板前(只)包含其声明。在模板稍后的位置(或者使用之后)包含模板定义;
- 在一个编译单元中,在使用模板前(只)包含其声明。在其他编译单元中包含其定义。
很遗憾的是C++并不支持第三种实现方式。
如果一个类模板的布局或者是一个内联函数模板的定义发生了改变,那么使用该类或者该函数的代码都要重新编译。
建议
- 使用模板用于很多实参类型的算法
- 用模板表示容器
- 注意template < class T>和template < typename T>意义相同
- 当设计一个模板时,首先设计和调试非模板版本,随后添加参数将其泛化
- 模板是类型安全的,但检查的时机太晚了
- 当设计一个模板时,仔细思考concept,它对模板参数的要求
- 如果一个类模板必须是可拷贝的,则为它定义一个非模板的拷贝构造函数和拷贝复制运算符
- 如果一个类模板必须是可移动的,则为它定义一个非模板的移动构造函数和移动复制运算符
- 虚函数不能是成员模板函数
- 只有当一个类型,依赖类模板的所有实参时才将其定义为模板成员
- 使用函数模板推断类模板实参类型
- 对多种不同的实参类型,重载函数模板来获得相同语义
- 借助实参带入失败机制为程序提供正确的候选函数集
- 使用模板别名简化符号,隐藏实现细节
- C++不支持模板分别编译,在每个用到模板的编译单元中都#include模板定义
- 使用普通函数作为接口编不能用模板处理的代码
- 将大的模板和较严重依赖上下文的模板分开编译
|