从本文开始,对 C++ Templates The Complete Guide Second Edition 进行系统学习和解读。
本章是对第一章模板函数的基本知识的讲解,后续章节会有更深的探讨。
函数模板惊鸿一瞥 A First Look at Function Templates
一个函数模板表示函数族,它的一些元素被参数化。
定义函数模板 Defining the Template
template<typename T>
T max (T a, T b) {
return b < a ? a : b;
}
该模板函数返回两个输入参数中的最大值,两个输入参数 a 和 b 的类型为模板参数 T 。模板参数按照如下语法形式进行申明:
template< comma-separated-list-of-parameters >
这里,关键字 typename 用于定义模板参数。由于历史原因,也可以使用关键字 class 代替 typename 来定义模板参数。在 C++98 之前是使用 class 申明模板参数,C++98 引入了 typename ,为了保持兼容,二者皆可。因此,上述代码等价如下:
template<class T>
T max (T a, T b) {
return b < a ? a : b;
}
但是,为了避免和类(class)的定义混淆,建议优先使用 typename 定义模板类型参数。
在我们的例子中模板参数为 T ,使用 T 表示只是惯例,你也可以使用其他标识符,例如 U 。这里的模板参数可以是任意类型,只要这种类型满足模板函数对其的操作。在上面的例子中,类型 T 必须支持 < 操作。此外,不太明显的一个限制是:为了被函数返回,T 类型的值必须是可以拷贝的。
使用函数模板 Using the Template
int main()
{
int i = 42;
std::cout << "max(7,i): " << ::max(7,i) << ’\n’;
double f1 = 3.4;
double f2 = -6.7;
std::cout << "max(f1,f2): " << ::max(f1,f2) << ’\n’;
std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << "max(s1,s2): " << ::max(s1,s2) << ’\n’;
return 0;
}
这里使用 ::max 调用函数 max 可以避免混淆,是为了在全局的命名空间查找 max ,因为标准库中也定义了该函数 std::max 。
使用不同参数类型的函数调用会产生不同的函数实例。上面的例子分别产生 int 、double 和 std::string 3 个版本的实例。例如 ::max(7,i) ,编译器会产生参数类型为 int 的函数实例,类似调用如下代码:
int max (int a, int b) {
return b < a ? a : b;
}
注意,void 也是有效的模板参数类型。例如:
template<typename T>
T foo(T*) {
}
void* vp = nullptr;
foo(vp);
两个阶段的翻译过程 Two-Phase Translation
使用模板中操作不支持的参数类型会产生编译报错。例如:
std::complex<float> c1, c2;
...
::max(c1,c2);
模板的翻译/编译分为两个阶段:分别发生在实例化前和实例化期间。
- 实例化前检查代码模板代码,这时候忽略模板参数。主要包括:
- 是否有语法错误。比如是否遗漏分号。
- 是否使用不依赖模板参数的未知名字(函数名、类型名等)。
- 检查不依赖模板参数的 static assert。
- 实例化期间再次检查模板代码,特别是依赖模板参数的部分。例如上面例子中实例化的参数类型是否支持
< 操作。
template <typename T>
void f(T x) {
undeclared();
undeclared(x);
static_assert(sizeof(int) > 10,
"int too small");
static_assert(sizeof(T) > 10,
"T too small");
}
模板参数推导 Template Argument Deduction
当我们使用参数调用模板函数(比如 max() ),模板参数就由我们传递的参数决定。如果我们传递两个 int 给模板函数,编译器将推导出 T 为 int 。
另外,T 可能是类型的一部分。例如,我们使用常量引用申明 max() :
template<typename T>
T max (T const& a, T const& b) {
return b < a ? a : b;
}
我们再次传递两个 int 给该模板函数,T 也被推导成 int 。
类型推导过程中的类型转换:
- 当通过引用申明调用参数时,类型推到过程中不会进行任何类型转化,两个被申明成相同模板参数
T 的参数必须完全匹配。例如:
#include<iostream>
template <typename T>
T max(T& a, T& b) {
return b < a ? a : b;
}
int main() {
const int a = 3;
int b = 4;
std::cout << ::max(a, b) << std::endl;
return 0;
}
编译报错如下:
hello.cpp: In function ‘int main()’:
hello.cpp:11:28: error: no matching function for call to ‘max(const int&, int&)’
11 | std::cout << ::max(a, b) << std::endl;
| ^
hello.cpp:4:3: note: candidate: ‘template<class T> T max(T&, T&)’
4 | T max(T& a, T& b) {
| ^~~
hello.cpp:4:3: note: template argument deduction/substitution failed:
hello.cpp:11:28: note: deduced conflicting types for parameter ‘T’ (‘const int’ and ‘int’)
11 | std::cout << ::max(a, b) << std::endl;
| ^
- 当通过值申明调用参数时,只支持退化(decay)的类型转换:cv(const 和 volatile)被忽略,引用转换为被引用的类型,原始数组和函数转换为相应的指针类型。两个被申明成相同模板参数
T 的参数的退化类型必须匹配。例如:
template<typename T>
T max (T a, T b);
...
int i = 5;
int const c = 42;
max(i, c);
max(c, c);
int& ir = i;
max(i, ir);
int arr[4];
foo(&i, arr);
但是下面的例子就有问题:
max(4, 7.2);
std::string s;
foo("hello", s);
解决上面这两个例子问题的方式如下:
- 显示强制类型转换。
max(static_cast<double>(4), 7.2); // OK - 显示指定模板参数以阻止编译器的类型推导。
max<double>(4, 7.2); // OK - 使用两个不同类型的模板参数。
函数默认调用参数的类型推导:
函数默认调用参数无法推导类型。看个例子:
template<typename T>
void f(T = "");
...
f(1);
f();
为了解决这个问题,需要申明模板参数默认类型。
template<typename T = std::string>
void f(T = "");
...
f();
多模板参数 Multiple Template Parameters
可以根据需要指定模板参数的个数。例如:
template<typename T1, typename T2>
T1 max (T1 a, T2 b) {
return b < a ? a : b;
}
...
auto m = ::max(4, 7.2);
从语法上看,上述代码没有任何问题。只是使用模板参数之一作为返回类型,那么调换下两个变量调用顺序,则会得到不同的结果。传递 66.66 和 42 给 max 得到 double 类型的 66.66 ,而传递 42 和 66.66 给 max 得到 int 类型的 66 。
C++ 提供了不同的方式解决上述问题:
- 引入第 3 个模板参数作为返回类型。
- 让编译器推导出返回类型。
- 使用两个参数的公共类型(common type)作为返回类型。
接下来分别讨论以上方式。
返回值类型的模板参数 Template Parameters for Return Types
在模板函数调用时,我们既可以显示指定模板参数类型,也可以不指定(交给编译器推导)。但是,当调用参数和模板参数之间没有联系的情况下,你必须在调用模板函数时显示指定模板参数。
template<typename T1, typename T2, typename RT>
RT max (T1 a, T2 b);
对于这种情况,编译器的参数推导无法处理返回值的类型,RT 也没有出现在函数调用参数中,因此 RT 无法被推导。你必须显示指定模板参数类型:
::max<int,double,double>(4, 7.2);
通常,你必须指定所有的参数类型直到最后一个无法被隐式推导的参数。对于这个例子,我们可以调整下返回值类型模板参数位置,这样我们可以在函数调用时候只指定返回值的类型,其他模板参数交给编译器推导。
template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b);
...
::max<double>(4, 7.2)
推导返回值类型 Deducing the Return Type
如果返回值类型依赖模板参数,最好的方法是交给编译器推导。C++14 可以直接使用 auto 作为返回值类型。
template<typename T1, typename T2>
auto max (T1 a, T2 b) {
return b < a ? a : b;
}
这里没有使用尾置返回类型(trailing return type)表明返回值类型可以根据函数体中返回值语句推导。在 C++11 中,需要使用尾置返回值类型声明:
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(b<a?a:b) {
return b < a ? a : b;
}
返回值的类型由问号表达式 b<a?a:b 的类型来确认。如果 a 和 b 的类型不同,decltype(b<a?a:b) 将返回二者的公共类型(common type)。关于问号表达式的返回类型可以参考 Conditional Operator: ? : ,关于公共类型可以参考 std::common_type 。
因此,这里问号表达式的条件并不重要,可以直接使用 true 代替:
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(true?a:b);
然后,上面的代码还有一个缺陷:返回值的类型可能是一个引用类型,因为有些条件下 T 可能是一个引用。因而需要改造如下:
#include <type_traits>
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> typename std::decay<decltype(true?a:b)>::type
{
return b < a ? a : b;
}
这里的 type 是一个成员类型,需要使用关键字 typename 进行修饰。
返回类型为通用类型 Return Type as Common Type
C++11 标准库提供了 std::common_type<>::type 产生两个(或多个)类型的公共类型(common type)。因此,我们可以使用 std::common_type<>::type 指定模板函数返回值类型:
#include <type_traits>
template<typename T1, typename T2>
typename std::common_type<T1,T2>::type max (T1 a, T2 b) {
return b < a ? a : b;
}
C++14 简化了 trait 库的使用,直接在 trait name 后加 _t 后缀。上面的返回值类型可以简化为:
std::common_type_t<T1,T2>
默认模板参数 Default Template Arguments
我们可以定义模板参数的默认值。例如上面的例子,我们可以定义 max 的返回值类型 RT ,并将其默认值定义为 T1 和 T2 的 common_type ,使用问号表达式实现如下:
#include <type_traits>
template<typename T1, typename T2,
typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
注意:以上实现,需要我们能够调用传入类型的默认构造函数。即 T1 和 T2 必须有默认构造函数。例如:
#include <type_traits>
class A {
public:
A() = default;
A(int a): x(a) {}
bool operator< (const A& a) {
return this->x < a.x;
}
private:
int x;
};
template<typename T1, typename T2,
typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
int main()
{
A a(2), b(3);
auto c = ::max(a, b);
return 0;
}
如果将 A() = default; 注释掉,则 A 没有默认构造函数,则导致模板默认参数类型推导失败。
main.cpp:15:44: error: no matching function for call to 'A::A()'
15 | typename RT = std::decay_t<decltype(true ? T1() : T2())>>
也可以使用 std::common_type 指定默认模板参数的公共类型:
#include <type_traits>
template<typename T1, typename T2,
typename RT = std::common_type_t<T1,T2>>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
我们可以使用默认模板参数作为返回值类型,也可以指定返回值类型:
auto a = ::max(4, 7.2);
auto b = ::max<double,int,long double>(7.2, 4);
如前面介绍的,我们可以调整默认模板参数位置到第一个,这样调用的时候可以只指定返回值类型,其他模板参数类型交给编译器推导。
template<typename RT = long, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
int i;
long l;
...
max(i, l);
max<int>(4, 42);
函数模板重载 Overloading Function Templates
像普通函数一样,函数模板也可以重载。例如:
int max (int a, int b)
{
return b < a ? a : b;
}
template<typename T>
T max (T a, T b)
{
return b < a ? a : b;
}
int main()
{
::max(7, 42);
::max(7.0, 42.0);
::max('a', 'b');
::max<>(7, 42);
::max<double>(7, 42);
::max('a', 42.7);
}
C++ 重载函数的匹配原则:如果模板实例化出的函数和普通重载函数都精确匹配,则优先选择普通重载函数,其次选择模板函数实例化出来的精确版本。关于这点可以参看我之前的博客 Item 26: Avoid overloading on universal references. 。
对于 ::max(7, 42); 普通函数和模板函数都可以匹配,优先匹配普通函数。对于 ::max(7.0, 42.0) 则会匹配到模板函数。
当然也可以显示指定一个空的模板参数列表,用于表明调用函数模板而不是普通函数:
::max<>(7, 42);
普通函数的参数可以自动进行类型转化,而函数模板不行。因而最后一个调用普通函数,两个参数都转为 int :
::max('a', 42.7);
对函数模板重载需要保证:对于任何调用,只有其中一个版本能够匹配,否则产生歧义。例如:返回值类型重载可能导致歧义。
template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
return b < a ? a : b;
}
template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
auto a = ::max(4, 7.2);
但是,auto b = ::max<long double>(7.2, 4); 则两个版本都可以匹配,导致编译报错:
main.cpp:17:33: error: call of overloaded 'max<long int, double>(int, double)' is ambiguous
17 | auto c = ::max<long, double>(4, 7.2);
| ~~~~~~~~~~~~~~~~~~~^~~~~~~~
main.cpp:4:6: note: candidate: 'auto max(T1, T2) [with T1 = long int; T2 = double]'
4 | auto max (T1 a, T2 b)
| ^~~
main.cpp:9:4: note: candidate: 'RT max(T1, T2) [with RT = long int; T1 = double; T2 = double]'
9 | RT max (T1 a, T2 b)
在看一个指针和传统 C 字符串重载的例子:
#include <cstring>
#include <string>
template<typename T>
T max (T a, T b)
{
return b < a ? a : b;
}
template<typename T>
T* max (T* a, T* b)
{
return *b < *a ? a : b;
}
char const* max (char const* a, char const* b)
{
return std::strcmp(b,a) < 0 ? a : b;
}
int main ()
{
int a = 7;
int b = 42;
auto m1 = ::max(a,b);
std::string s1 = "hey";
std::string s2 = "you";
auto m2 = ::max(s1,s2);
int* p1 = &b;
int* p2 = &a;
auto m3 = ::max(p1,p2);
char const* x = "hello";
char const* y = "world";
auto m4 = ::max(x,y);
}
一般建议使用传值的函数模板,如果实现的传引用的模板函数,再重载传 C 字符串值版本,可能导致错误:
#include <cstring>
template<typename T>
T const& max (T const& a, T const& b)
{
return b < a ? a : b;
}
char const* max (char const* a, char const* b)
{
return std::strcmp(b,a) < 0 ? a : b;
}
template<typename T>
T const& max (T const& a, T const& b, T const& c)
{
return max (max(a,b), c);
}
int main ()
{
char const* s1 = "frederic";
char const* s2 = "anica";
char const* s3 = "lucas";
auto m2 = ::max(s1, s2, s3);
}
max(max(a, b), c) 中,max(a, b) 产生了一个临时对象的引用,这个临时引用在 return max (max(a,b), c); 语句结束就失效了,导致引用悬挂:
main.cpp: In instantiation of 'const T& max(const T&, const T&, const T&) [with T = const char*]':
main.cpp:24:17: required from here
main.cpp:17:20: warning: returning reference to temporary [-Wreturn-local-addr]
17 | return max (max(a,b), c); // error if max(a,b) uses call-by-value
| ~~~~^~~~~~~~~~~~~
bash: line 7: 2907 Segmentation fault (core dumped) ./a.out
此外,重载版本需要在调用前申明,否则可能产生不符合预期的结果。例如:
#include <iostream>
template<typename T>
T max (T a, T b)
{
std::cout << "max<T>() \n";
return b < a ? a : b;
}
template<typename T>
T max (T a, T b, T c)
{
return max (max(a,b), c);
}
int max (int a, int b)
{
std::cout << "max(int,int) \n";
return b < a ? a : b;
}
int main()
{
::max(47,11,33);
}
此外,C++11 可以使用 constexpr 函数来生成编译期值。
template <typename T, typename U>
constexpr auto max(T a, U b) {
return b < a ? a : b;
}
int a[::max(sizeof(char), 1000u)];
std::array<int, ::max(sizeof(char), 1000u)> b;
总结 Summary
- 函数模板定义了一个函数族
- 编译器可以根据传递的参数推导模板参数
- 也可以显示指定模板参数
- 可以定义默认模板参数
- 可以重载函数模板
- 重载模板函数时,需要确保对于任何调用只有一个匹配版本
- 重载函数模板时,注意传引用和传值版本导致的引用悬挂问题
- 重载版本需要在调用前申明
参考 Reference
- http://www.tmplbook.com
- https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/e4213hs1%28v=vs.110%29?redirectedfrom=MSDN
- https://en.cppreference.com/w/cpp/types/common_type
|