一,函数模板
1.基础概念
模板编程是C++中泛型编程的基础。
一个模板可以是创建类或者函数的蓝图。
模板编程分两种,分别是算法抽象的模板、数据抽象的模板。算法抽象的模板以函数模板为主,数据抽象的模板以类模板为主。
基于函数模板生成的函数定义被称为模板的一个实例。
模板的定义以关键字template开始,后跟一个由尖括号"<>"括起来的模板参数列表。
2.函数模板的简单样例
函数模板的开头:template
定义模板参数的关键字:typename
模板参数样例:T1, T2
函数参数样例:a, b
template <typename T1, typename T2> | |
void func(T1 a, T2 b) | |
{ | |
//process code | |
} |
补充:在C++98标准添加关键字typename之前,C++也可以使用关键字class来为函数模板创建模板参数列表。
代码样例:
template <class T> | |
void Swap(T &a, T &b) | |
{ | |
T temp; | |
temp = a; | |
a = b; | |
b = temp; | |
} |
3.函数模板的实例化
函数模板的实例化是指,编译器根据函数模板和具体的数据类型生成函数定义。
函数模板在实例化以后,模板参数会变成具体的数据类型,比如int, char等。
对于某一种具体的数据类型,比如int,无论以这个数据类型调用多少次函数模板,最后只生成一次该类型的模板实例。
所以,对于相同的数据类型,第一次调用函数模板的时候才会生成实例,后面再次调用的时候,都是直接使用该实例。
当编译器遇到一个函数模板的定义时,并不会马上生成相关代码,只有当我们将函数模板实例化成一个函数定义时,编译器才会生成代码。
代码样例:
a.函数模板
template <typename T> | |
T add(T num1, T num2) { | |
return (num1 + num2); | |
} |
b.函数模板的实例化
int result1 = add<int>(2, 3); | |
double result2 = add<double>(2.2, 3.3); |
实例化过程的图示:
在项目工程中,我们通常将类的定义放在头文件中,将类的成员函数的定义放在源文件中,将普通函数的声明放在头文件中,将普通函数的定义放在源文件中,但是函数模板的规则和它们不一样。
为了让编译器为实例化后的函数模板生成代码,编译器需要同时知道函数模板的声明和定义,因此函数模板的定义也需要放在头文件中。
4.函数模板的引用传参
对于以下函数模板:
template <typename T> | |
T larger(T a, T b) | |
{ | |
return a > b ? a : b; | |
} |
该函数模板实例化以后生成的函数,需要按值传递的方式接收实参。
由于按值传送对象,会导致不必要地复制这些对象,因此,推荐使用const引用的方式定义模板参数。
template <typename T> | |
const T& larger(const T& a, const T& b) | |
{ | |
return a > b ? a : b; | |
} |
5.函数模板的返回类型推断
对于无返回值的函数模板,可以把返回值类型写为void,比如最开始提到的:
template <typename T1, typename T2> | |
void func(T1 a, T2 b) | |
有的函数模板,返回值类型和参数一致,同为T,比如:
template <typename T> | |
T larger(T a, T b) |
但是,当返回值类型和参数不一致时,得想办法让编译器可以推断返回值类型。
最简单的方式是使用auto关键字。
template <typename T1, typename T2> | |
auto larger(const T1& a, const T2& b) | |
{ | |
return a > b ? a : b; | |
} |
但是,使用auto来推导函数的返回值类型时,会默认去掉引用和const限定符,因此,以上方式会导致返回值发生不必要的复制。
因此,为了让返回值被const修饰,且采取引用的方式来传值,需要显式地加上"const &",以上代码可以改为:
template <typename T1, typename T2> | |
const auto& larger(const T1& a, const T2& b) | |
{ | |
return a > b ? a : b; | |
} |
还有一种更好的方式,C++11标准引入了decltype关键字,decltype相当于"const auto&",因为decltype在做类型推导时,不会去掉引用和const限定符。
但是decltype的用法不能像auto一样,直接放在函数名前面。
decltype用法分两种:
方式1.拖尾方式:decltype(返回值相关代码)
template <typename T1, typename T2> | |
auto larger(T1 a, T2 b) -> decltype(a > b ? a : b) | |
{ | |
return a > b ? a : b; | |
} |
方式2.和auto关键字结合:decltype(auto)
template <typename T1, typename T2> | |
decltype(auto) larger(T1 a, T2 b) | |
{ | |
return a > b ? a : b; | |
} |
第一种用法需要把返回值相关的代码逻辑重复写一遍,第二种用法更简洁。
6.模板参数可以指定默认值
可以用具体的数据类型为模板参数指定默认值。
例如:当函数经常使用int类型的参数时,指定模板参数的默认值为int。
template <typename T1=int, typename T2> | |
void func(T1 a, T2 b) |
7.非类型的模板参数
模板参数分两种:
1.类型模板参数
2.非类型模板参数
由尖括号"<>"括起来的模板参数列表中,除了可以包含类型模板参数,还可以包含非类型模板参数。
以上提到的"typename T1, typename T2"中的"T1, T2"都属于类型模板参数,而"int n, float m"中的"n, m"都属于类型模板参数非类型模板参数。
类型模板参数经过实例化会变成具体类型。
非类型模板参数经过实例化会变成具体的值。
代码样例:
应用场景:比较不同长度的字符串字面常量。
函数模板定义了两个非类型模板参数,参数N表示第一个数组的长度,参数M表示第二个数组的长度。
数组采用const和引用的方式传参。
template<int N, int M> | |
int compare(const char (&p1)[N], const char (&p2)[M]) | |
{ | |
return strcmp(p1, p2); | |
} |
非类型模板参数可以使用的数据类型:
整型,如int、long等 | |
枚举类型 | |
对象类型的引用或指针 | |
函数的引用或指针 | |
类成员的指针 |
当模板参数列表中,同时有类型模板参数和非类型模板参数时,建议将非类型模板参数写在类型模板参数的前面。
代码样例:
template <int lower, int upper, typename T> | |
bool is_in_range(const T& value) | |
{ | |
return (value <= upper) && (value >= lower); | |
} |
完整代码样例:
求任意数据类型,任意大小的数组的平均值。
template <typename T, int N> | |
T average(const T(&array)[N]) | |
{ | |
T sum{}; | |
int i; | |
for (i = 0; i < N; ++i) | |
{ | |
sum += array[i]; | |
} | |
return sum / N; | |
} | |
int main() | |
{ | |
double array_1[2]{ 1.1, 2.1 }; | |
std::cout << average(array_1) << std::endl; | |
float array_2[]{ 1.0, 2.0, 3.0, 4.0 }; | |
std::cout << average(array_2) << std::endl; | |
int array_3[] = { 1, 2, 3, 4 }; | |
std::cout << average(array_3) << std::endl; | |
return 0; | |
} |
运行结果:
1.6 | |
2.5 | |
2 |
8.inline/constexpr修饰的函数模板
和具体函数一样,函数模板可以用inline或constexpr修饰。
inline或constexpr在修饰时放在模板参数列表之后,返回值类型之前。
代码样例:
template <typename T> | |
inline T min(const T&, const T&); |
9.函数模板的重载
函数模板的重载有两种方式:
方式1.用同名函数重载函数模板
方式2.用另一个函数模板重载已有模板
重载的代码样例:
template <typename T> | |
T larger(const T data[], size_t count) | |
{ | |
T result {data[0]}; | |
for (size_t i {1}; i < count; ++i) | |
{ | |
if (data[i] > result) | |
result = data[i]; | |
} | |
return result; | |
} | |
template <typename T> | |
T larger(const std::vector<T>& data) | |
{ | |
T result {data[0]}; | |
for (auto& value : data) | |
{ | |
if (value > result) | |
result = value; | |
} | |
return result; | |
} |
二,函数模板的特例
1.基础概念
函数模板的特例是由原始的函数模板具体化而来的,因此,函数模板的特例也被称为函数模板的具体化(explicit specialization)。
函数模板的特例的定义必须放在函数模板的声明和定义之后。
当编译器找到与函数调用匹配的具体化定义时,将直接使用该函数模板的特例,而不再实例化函数模板。
函数模板的特例也以关键字template开头,但要省略参数,所以template后面的尖括号是空的。
函数模板的特例的定义需要传递具体的参数类型。
当函数模板的某个实例,需要被定义一种不同于原始函数模板的行为,就可以使用函数模板的特例去定义。
空的尖括号“<>”表示编译器不需要做类型推导。
函数模板特例的简单样例:
template <> | |
void func(int a, double b) | |
{ | |
//process code | |
} |
2.代码样例
给定函数模板 larger(T1 a, T2 b)
template <typename T1, typename T2> | |
decltype(auto) larger(T1 a, T2 b) | |
{ | |
return a > b ? a : b; | |
} |
由于该函数模板不适用于指针数据类型,因此,定义以下函数模板的特例。
函数模板的特例,在代码逻辑中相比原始的函数模板多了解引用操作。
template <> | |
int* larger<int*>(int* a, int* b) | |
{ | |
return *a > *b ? a : b; | |
//解引用操作是为了让两个指针比较指向的数值而不是地址 | |
} |
普通函数,函数模板,函数模板特例的代码形式
//function | |
void Swap(int& a, int& b); | |
//template prototype | |
template <typename T> | |
void Swap(T& a, T& b); | |
//template explicit specialization | |
template <> | |
void Swap<int>(int& a, int& b); |
3.编译时的匹配优先级
当某个具体的数据类型可以同时匹配上普通函数,函数模板,函数模板的特例时,普通函数的调用优先于函数模板特例,函数模板特例的调用优先于原始函数模板。