1. 基本概念
函数模板的格式如下所示:
template<typename T>
T max (T a, T b)
{
return b < a ? a : b;
}
含义是一目了然的事情,但是需要注意:
- 传入的ab必须支持运算符<
- ab必须是可以拷贝的,否则没法返回
具体的使用实例如下:
template<typename T>
T max (T a, T b)
{
return b < a ? a : b;
}
int main()
{
int i = 42;
std::cout << ::max(1, i) << '\n';
double f1 = 3.14;
double f2 = -3.14;
std::cout << ::max(f1, f2) << '\n';
std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << ::max(s1, s2) << '\n';
}
需要注意两点:
- 传入的参数类型必须一致,否则会报错,比如
::max(5.5,1)
,解决这个问题可以用::max(5.5,1)
进行隐式转化 - 最好使用
::
标识符,确保函数max()
是属于全局空间,避免和std::max()
混淆
我们在编译时,把模板T
推导为int
的过程叫做实例化(instantiation),而int
这个具体的类型就叫做模板的实例(instance)。
同时需要注意void也是合法的模板参数之一!
template<typename T>
T foo(T*) { }
void* vp = nullptr;
foo(vp); // OK: deduces void
foo(void*)
另外一个很重要的点是模板的双重编译(Two-phase Translation),实际上模板在编译检查时经历了两个阶段:
- 在没有实例化时,称为定义时(definition time)。这时编译器会主要检查语法错误,未定义行为,静态声明等等问题。
- 实例化时(instantiation time),编译器会将模板实例带入重新检查一遍是否合法。
2. 参数推断
这一部分原书讲的比较简单,其实”Effective Modern C++”讲的挺好的,我之前也做了笔记,这里我直接照搬那本书上的内容。
在做之前我们先明确几个术语,下面的示范代码包含了:
- param,形参
- paramtype,形参的类型,这里是const T&
- expr,实参,expression的缩写
- CV符,const,volatile
- 引用符,&
template<typename T>
void f(const T& param);
int expr = 0;
f(expr); // call f with an int
然后我们分几种情况讨论:
(1)左值引用
步骤:
- 忽略expr中paramtype包含的部分得到T
- 将T与paramtype配对的到param
template<typename T> void f(T& param); // param is a reference
int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int
f(x); // T is int, param's type is int&
f(cx); // T is const int,
// param's type is const int&
f(rx); // T is const int,
// param's type is const int&
//////////////////////////////
template<typename T> void f(const T& param); // param is now a ref-to-const
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T is int, param's type is const int&
f(cx); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&
////////////////////////////////////
//指针也适用这个原则
template<typename T> void f(T* param);
// param is now a pointer
int x = 27; // as before
const int *px = &x; // px is a ptr to x as a const int
f(&x); // T is int, param's type is int*
f(px); // T is const int,
// param's type is const int*
(2)右值引用
步骤:
- 如果expr是左值,T和param都被推为左值并加上引用(不管有没有统统加上引用符)
- 如果是右值,T不变,
param
加上&&。
示例:
template<typename T> void f(T&& param);
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // x is lvalue, so T is int&,
// param's type is also int&
f(cx); // cx is lvalue, so T is const int&,
// param's type is also const int&
f(rx); // rx is lvalue, so T is const int&,
// param's type is also const int&
f(27); // 27 is rvalue, so T is int,
// param's type is therefore int&&
(3)值传递
这就是最基本的值传递。意味着,param
复制了一个传入的参数。他的哲学就是忽略忽略再忽略:
- 如果
expr
是引用,忽略引用的部分 - 如果忽略引用后,
expr
是const
类型或volatile
类型,忽略。
template<typename T> void
f(T param); // param is now passed by value
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int
注意:只是忽略引用,指针还是不变。
template<typename T> void f(T param);
// param is still passed by value
const char* const ptr ="Fun with pointers";
// ptr is const pointer to const object
f(ptr);
//T is const char* const
(4)退化
数组类型和指针类型是完全不一样的(虽然他们在使用时可以混用)。由于C语言老祖宗的继承关系,C++依然保留了这些特性,导致很多人误以为数组和指针参数是一样的。在传递过程中,数组会退化为它第一个元素的指针。
template<typename T> void f(T param);
const char name[] = "J. P. Briggs"; // name's type is const char[13]
const char * ptrToName = name; // array decays to pointer
f(name); // name is array, but T deduced as const char*
这种退化导致有用的信息(数组长度)丢失。然而声明引用,可以使得模板推断保留成数组的形式:
template<typename T>
void f(T& param);
f(name); //deduce to const char[13]
在这个例子中T
被推导为const char[13],param
则被推导为const char(&)[13]。如果改为T&&
,则T
和param
都被推为const char(&)[13]
讲完数组,再来讲讲函数。在C++中不止是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组的全部讨论都可以应用到函数来:
void someFunc(int, double); // someFunc is a function;
// type is void(int, double)
template<typename T> void f1(T param); // in f1, param passed by value
template<typename T> void f2(T& param); // in f2, param passed by ref
f1(someFunc); // param deduced as ptr-to-func;
// type is void (*)(int, double)
f2(someFunc); // param deduced as ref-to-func;
// type is void (&)(int, double)
3. 多模板参数
多模板参数的形式是简单的,但细节需要考究。
template<typename T1, typename T2>
T1 max (T1 a, T2 b)
{
return b < a ? a : b;
}
auto m = ::max(1, 3.14); // 返回类型由第一个实参决定
上面的例子如果改为单模板参数则无法成功匹配。
而如果改为多模板参数,则调用过程发生了隐式转化,返回类型由第一个参数决定。如果我们把1和3.14调换位置,则返回的是double类型而不是int。
我们可以通过指定返回参数模板来解决这个问题。这段代码我们用尖括号指定模板类型,对应的位置就是RT,这样我们就能顺利返回我们想要的类型了。
template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b);
{
return b < a ? a : b;
}
::max<double>(1, 3.14); // 返回类型为double, T1和T2被推断
在现代CPP体系中,我们可以自动推导返回类型。下面这段代码展示了11和14的一些区别。当我们调用::max(5, 1.2)
的时候,5被隐式转化为double类型,返回的结果也是double类型。
template<typename T1, typename T2>
// FOR c++14
auto max (T1 a, T2 b)
{
return b < a ? a : b;
}
//FOR c++11
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(b < a ? a : b)
{
return b < a ? a : b;
}
但是,有时候这有可能会导致返回引用类型,因为paramtype可能就写为引用类型,我们需要调用decayed
去除引用符号(需要头文件
template<typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true ? a : b)>::type
{
return b < a ? a : b;
}
现代CPP中引入了Common Type这一概念,相当于最大兼容。
common_type<int, float>::type // float,因为int可以转换成float
common_type<int, float, double>::type // double,因为int, float都可以转换成double
利用这个特性,我们能够进一步改造模板。(注意一下14那个有_t,11那个没有)
// for C++14
std::common_type_t<T1,T2> max(T1 a,T2 b)
{
return a>b?a:b;
}
// for C++11
std::common_type<T1,T2>::type max(T1 a,T2 b)
{
return a>b?a:b;
}
4. 函数模板的重载
具有相同名称的非函数模板可以和函数模板共存,利用这个特性我们能实现模板函数的重载。
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(1, 42); // 调用非模板的函数
::max(1.0, 3.14); // 通过推断调用max<double>
::max('a', 'b'); // 通过推断调用max<char>
::max<>(1, 42); // 通过推断调用max<int>
::max<double>(1, 42); // 调用max<double>,不推断
}
上面的代码展示了编译器的特性:选择最优匹配的函数。
重载模板的原则是需要保证只有一个合理匹配,否则会造成歧义,导致无法通过编译。下面的代码中,由于double可以被隐式转化为int,所以会导致歧义。
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(1, 3.14); // 调用第一个模板
auto b = ::max<long double>(3.14, 1); // 调用第二个模板
auto c = ::max<int>(1, 3.14); // 错误:两个模板都匹配