目录
模板是C++中泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。当使用一个vector这样的泛型类型,或者find这样的泛型函数时,我们提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译时。
当没有模板时,为了实现一个通用的交换函数,需要定义多个重载函数。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
//...
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
1. 函数模板
1.1 函数模板的定义
我们可以定义一个通用的函数模板(function template),而不是为每个类型都定义一个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
函数模板的格式:
template<typename T1, typename T2, ..., typename Tn>
返回类型 函数名(参数列表)
{
// 函数体
}
- 模板定义以关键字template开始。
- typename T1, typename T2, ..., typename Tn是模板参数列表(template parameter list),用<>包围起来,模板参数列表不能为空。
- typename是用来定义模板参数关键字,也可以使用class(不能使用struct代替class)。
template<class T1, class T2> // ok
template<typename T1, typename T2> // ok
template<class T1, typename T2> // ok
定义一个通用的交换函数模板:
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
1.2 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1.2.1 隐式实例化
让编译器根据实参推演模板参数的实际类型。
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int i1 = 10, i2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(i1, i2); // ok T确定为int类型
Add(d1, d2); // ok T确定为double类型
Add(i1, d1); // err T不确定 -> 函数模板不允许自动类型转换
Add(i1, (int)d1); // ok T确定为int类型,因为d1被强制类型转换为int类型了
Add((double)i2, d2); // ok T确定为double类型,因为a2被强制类型转换为double类型了
return 0;
}
1.2.2 显式实例化
在函数名后的<>中指定模板参数的实际类型。
int main()
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
1.3 函数模板的重载
函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数。
如果涉及函数模板,则函数匹配规则会在以下几方面受到影响:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
- 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
- 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:
1)如果同样好的函数中只有一个是非模板函数,则选择此函数。
2)如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
3)否则,此调用有歧义。
// 函数模板
template<class T>
T Add(const T& left, const T& right)
{
cout << "函数模板" << endl;
return left + right;
}
// 重载的非模板函数
int Add(const int& left, const int& right)
{
cout << "非模板函数" << endl;
return left + right;
}
int main()
{
int i1 = 10, i2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(i1, i2); // 调用重载的非模板函数
Add(d1, d2); // 调用函数模板
Add<int>(i1, i2); // 调用函数模板
Add<double>(d1, d2); // 调用函数模板
Add<double>(i2, d2); // 调用函数模板
Add(i1, d1); // 调用重载的非模板函数,函数模板不允许自动类型转换,普通函数允许自动类型转换
Add(i1, (int)d1); // 调用重载的非模板函数
Add((double)i2, d2); // 调用函数模板
return 0;
}
2. 类模板
2.1 类模板的定义
类模板(class template)是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。
类模板的格式:
template<typename T1, typename T2, ..., typename Tn>
class 类名
{
// 类体
};
- 模板定义以关键字template开始。
- typename T1, typename T2, ..., typename Tn是模板参数列表(template parameter list),用<>包围起来,模板参数列表不能为空。
- typename是用来定义模板参数关键字,也可以使用class(不能使用struct代替class)。
template<class T1, class T2> // ok
template<typename T1, typename T2> // ok
template<class T1, typename T2> // ok
栈的实现:
template<class T>
class Stack
{
public:
Stack(int capaicty = 4)
: _a(new T[capaicty])
, _top(0)
, _capacity(capaicty)
{}
~Stack()
{
delete[] _a;
_capacity = _top = 0;
}
private:
T* _a;
size_t _top;
size_t _capacity;
};
2.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Stack类名,Stack<int>才是类型
Stack<int> st1;
Stack<double> st2;
3. 非类型模板参数
上面我们定义的Swap函数模板和Stack类模板的模板参数都表示类型,称为类型参数(type parameter)。
除了定义类型参数,还可以在模板中定义非类型参数(nontype parameter)。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式(不会改变并且在编译过程就能得到计算结果的表达式),从而允许编译器在编译时实例化模板。
例如,我们可以编写一个compare版本处理字符串字面常量。这种字面常量是const char的数组。由于不能拷贝一个数组,所以我们将自己的参数定义为数组的引用。由于我们希望能比较不同长度的字符串字面常量,因此为模板定义了两个非类型的参数。第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度:
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
当我们调用这个版本的compare时:
compare("hi", "mom");
编译器会使用字面常量的大小来代替N和M,从而实例化模板。编译器会实例化出如下版本:
int compare(const char (&p1)[3], const char (&p2)[4])
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。
在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。
4. 模板编译
当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用(而不是定义)模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。
通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
模板不支持分离式编译,应该将声明和定义放到一个头文件里。
5. 模板特化
5.1 模板特化的概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。
template<typename T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; //可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; //可以比较,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; //可以比较,结果错误
return 0;
}
可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
5.2 函数模板特化
步骤:
- 必须要先有一个基础的函数模板。
- 关键字template后面接一对空的尖括号<>。
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
- 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
当定义函数模板的特化版本时,我们本质上接管了编译器的工作。即,我们为原模板的一个特殊实例提供了定义。重要的是要弄清:一个特化版本本质上是一个实例,而非函数名的一个重载版本。
// Less函数模板
template<typename T>
bool Less(T left, T right)
{
return left < right;
}
// Less函数模板的特化版本
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl;
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 调用特化版本
return 0;
}
一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
bool Less(Date* left, Date* right)
{
return *left < *right;
}
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
5.3 类模板特化
5.3.1 全特化
全特化:将模板参数列表中所有的参数都确定化。
template<typename T1, typename T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 全特化
template<>
class Data<int, char>
{
public:
Data() { cout << "Data<int, char>" << endl; }
private:
int _d1;
char _d2;
};
int main()
{
Data<int, int> d1;
Data<int, char> d2;
return 0;
}
5.3.2 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
比如对于以下模板类:
template<typename T1, typename T2>
class Data
{
public:
Data() {cout << "Data<T1, T2>" << endl;}
private:
T1 _d1;
T2 _d2;
};
偏特化有以下两种表现方式:
- 部分特化:将模板参数类表中的一部分参数特化。
// 将第二个参数特化为int
template<typename T1>
class Data<T1, int>
{
public:
Data() {cout << "Data<T1, int>" << endl;}
private:
T1 _d1;
int _d2;
};
- 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
// 两个参数偏特化为指针类型
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:
Data() {cout << "Data<T1*, T2*>" << endl;}
private:
T1 _d1;
T2 _d2;
};
// 两个参数偏特化为引用类型
template<typename T1, typename T2>
class Data<T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1 & _d1;
const T2 & _d2;
};
int main()
{
Data<double, int> d1;
Data<int, double> d2;
Data<int*, int*> d3;
Data<int&, int&> d4(1, 2);
return 0;
}
6. 可变参数模板
6.1 可变参数模板的概念
一个可变参数模板(variadic template)就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包( parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包(function parameter packct),表示零个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class...或 typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
template<class... Args>
void ShowList(Args... args)
{}
6.2 递归函数方式展开参数包
// 递归终止函数
template<class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template<class T, class... Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
6.3 逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的,printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
// 展开函数
template<class...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}