函数模板
文章目录
- 函数模板
- 定义函数模板
- 使用函数模板
- 样例
- 两阶段翻译 Two-Phase Translation
- 模板的编译和链接问题
- 多模板参数
- 引入额外模板参数作为返回值类型
- 让编译器自己找出返回值类型
- 将返回值声明为两个模板参数的公共类型
- 样例
- 默认模板参数
- 样例
- 重载函数模板
- 模板函数特化
- 非类型模板参数
定义函数模板
template<typename T>
T max(T a,T b) {
return b < a ? a : b;
}
使用函数模板
std::cout << max(7,42) << std::endl;
std::cout << max(1.1,2.2) << std::endl;
std::cout << max("math","mathematics") << std::endl;
模板不是被编译成可以处理任何类型的单个函数。相反,编译器会针对每一个使用该模板的类型生成对应的函数。例如,max(7,42)
的调用在语义上相当于调用了:
int max(int a,int b) {
return b < a ? a : b;
}
double、string同理。
样例
// 函数模板的推断
#include <iostream>
using namespace std;
template <typename T>
void Function(T arg)
{
cout << "template<T> " << arg << endl;
}
void Function(int arg)
{
cout << "oridary <int> " << arg << endl;
}
int main(int argc, char **argv)
{
// oridary <int> 13
// template<T> 123
// template<T> 13
// template<T> 13
Function(13);
// 当普通函数和函数模板都符合时,普通函数的优先级更高
Function("123");
Function<int>(13); // 可以强制使用模板
Function<>(13); // 可以自动推导
}
将模板参数替换成具体参数类型的过程叫做instantiation
,这个过程会产生一个instance of template
。
两阶段翻译 Two-Phase Translation
如果某一特定参数类型不支持模板内的操作,那么编译阶段会报错,例如:
std::complex<float> c1,c2; //不支持 max中的 < 操作,编译阶段会报错
...
max(c1,c2);
模板会分成两个阶段进行”编译“: 1. 在不进行模板instantiation
的definition time
阶段,此时会忽略模板参数,检查如下方面: * 语法错误,包括缺失分号。 * 使用未定义参数。 * 如果static assertion不依赖模板参数,会检查是否通过static assertion. 2. 在instantiation
阶段,会再次检查模板里所有代码的正确性,尤其是那些依赖模板参数的部分。
例如:
template<typename T>
void foo(T t) {
undeclared(); // first-phase compile-time error if undeclared() unknown
undeclared(t); // second-phase compile-time error if undeclared(T) unknown
static_assert(sizeof(int) > 10,"int too small"); // first-phase compile-time error
static_assert(sizeof(T) > 10, "T too small"); // second-phase compile-time error
}
模板的编译和链接问题
大多数人会按照如下方式组织非模板代码: 将类或者其他类型声明放在头文件(.hpp、.H、.h、.hh、.hxx)中。 将函数定义等放到一个单独的编译单元文件中(.cpp、.C、.c、.cc、.cxx)。
但是这种组织方式在包含模板的代码中却行不通,例如: 头文件:
// myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template<typename T>
void printTypeof (T const&);
#endif // MYFIRST_HPP
定义函数模板的文件:
// myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// implementation/definition of template
template<typename T>
void printTypeof (T const& x) {
std::cout << typeid(x).name() << '\n';
}
在另一个文件中使用该模板:
// myfirstmain.cpp
#include "myfirst.hpp"
// use of the template
int main() {
double ice = 3.0;
printTypeof(ice); // call function template for type double
}
在c/c++中,当编译阶段发现一个符号(printTypeof)没有定义只有声明时,编译器会假设它的定义在其他文件中,所以编译器会留一个”坑“给链接器linker,让它去填充真正的符号地址。
但是上面说过,模板是比较特殊的,需要在编译阶段进行instantiation
,即需要进行模板参数类型推断,实例化模板,当然也就需要知道函数的定义。但是由于上面两个cpp文件都是单独的编译单元文件,所以当编译器编译myfirstmain.cpp
时,它没有找到模板的定义,自然也就没有instantiation
。
解决办法就是我们把模板的声明和定义都放在一个头文件。大家可以看一下自己环境下的vector等STL源文件,就是把类的声明和定义都放在了一个文件中。
多模板参数
template<typename T1, typename T2>
T1 max (T1 a, T2 b) {
return b < a ? a : b;
}
...
auto m = max(4, 7.2); // 注意:返回类型是第一个模板参数T1 的类型
但是问题正如注释中说的,max的返回值类型总是T1。如果我们调用max(42, 66.66)
,返回值则是66。
一般有三个方法解决这个问题:
- 引入额外模板参数作为返回值类型
- 让编译器自己找出返回值类型
- 将返回值声明为两个模板参数的公共类型,比如int和float,公共类型就是float
引入额外模板参数作为返回值类型
在函数模板的参数类型推导过程中,一般我们不用显式指定模板参数类型。但是当模板参数不能根据传递的参数推导出来时,我们就需要显式的指定模板参数类型。
template<typename T1, typename T2, typename RT>
RT max(T1 a, T2 b);
RT是不能根据函数的参数列表推导出来的,所以我们需要显式的指定:
max<int, double, double>(4, 7.2);
或者我们改变模板参数列表顺序,这种情况只需显式的指定一个参数类型即可:
template<typename RT typename T1, typename T2> //RT变为第一个模板参数
RT max(T1 a, T2 b);
...
max<double>(4, 7.2);
让编译器自己找出返回值类型
在C++11中,我们可以利用auto和trailing return type来让编译器找出返回值类型:
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b) {
return b < a ? a : b;
}
decltype后面的文章会讲到,这里只需知道它可以获取到表达式的类型。
我们可以写的更简单点:
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(true ? a : b) { // true ? a : b
return b < a ? a : b;
}
关于?:返回值规则可以参考这个:Conditional Operator: ? :
看到true ? a : b
不要奇怪为什么是true,这里的重点不是计算返回值,而是得到返回值类型。
在C++14中,我们可以省略trailing return type:
template<typename T1, typename T2>
auto max (T1 a, T2 b) {
return b < a ? a : b;
}
将返回值声明为两个模板参数的公共类型
c++11新特性std::common_type
可以产生几个不同类型的共同类型,其实核心意思跟上面说的差不多:
template <typename T1, typename T2>
typename std::common_type<T1, T2>::type max(T1 a, T2 b) {
return b < a ? a : b;
}
在c++14中,可以更简单的写:
template <typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2 b) {
return b < a ? a : b;
}
这里使用_t
后缀让我们不用写typename
和::type
。类似的还有_v,这个在c++14的type traits
里很常见。
样例
// /// <summary>
// ///引入额外模板参数作为返回值类型
// 让编译器自己找出返回值类型
// 将返回值声明为两个模板参数的公共类型,比如int和float,公共类型就是float
#include <iostream>
using namespace std;
namespace test01
{
template <typename RT, typename T2, typename T>
RT add(const T2 &x, const T &y)
{
return x + y;
}
}
namespace test02
{
template <typename T2, typename T>
auto add(const T2 &x, const T &y) -> decltype(x + y)
{
return x + y;
}
}
namespace test03
{
template <typename T2, typename T>
typename std::common_type<T2, T>::type add(const T2 &x, const T &y)
{
return x + y;
}
}
namespace test04
{
template <typename T2, typename T>
std::common_type_t<T2, T> add(const T2 &x, const T &y)
{
return x + y;
}
}
int main(int argc, char **argv)
{
/// @brief 引入额外模板参数作为返回值类型
/// @param argc
/// @param argv
/// @return
cout << test01::add<float, float, float>(11.2, 22.2) << endl;
cout << test01::add<int, float, float>(11.2, 22.2) << endl;
/// @brief 让结果自己推断返回值,让编译器自己找出返回值类型
/// @param argc
/// @param argv
/// @return
cout << test02::add<float, int>(22.5, 35) << endl;
cout << test02::add<int, int>(22, 35) << endl;
/// @brief 将返回值声明为两个模板参数的公共类型
/// @param argc
/// @param argv
/// @return
cout << test03::add<float, int>(22.5, 35) << endl;
cout << test03::add<int, int>(22, 35) << endl;
/// @brief 这里使用_t后缀让我们不用写typename和::type
/// @param argc
/// @param argv
/// @return
cout << test04::add<float, int>(22.5, 35) << endl;
cout << test04::add<int, int>(22, 35) << endl;
}
默认模板参数
这个很像函数的默认参数,直接看例子:
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);
正如第二个用法,如果我们想显示的指明RT的类型,必须显示的指出全部三个参数类型。但是与函数默认参数不同的是,我们可以将默认参数放到第一个位置:
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); // 返回值类型是long (RT 的默认值)
max<int>(4, 42); //返回int,因为其被显式指定
样例
#include <iostream>
using namespace std;
namespace test01
{
template <typename T1, typename T2, typename RT = std::common_type_t<T1, T2>>
RT max(T1 a, T2 b)
{
return b < a ? a : b;
}
}
namespace test02
{
template <typename RT = long, typename T1, typename T2> //
RT max(T1 a, T2 b)
{
return b < a ? a : b;
}
}
int main(int argc, char **argv)
{
/// @brief 函数模板的默认参数
/// @param argc
/// @param argv
/// @return
auto a = test01::max(4, 7.2);
auto b = test01::max<double, int, long double>(7.2, 4);
auto c = test02::max(4, 7.2);
auto d = test02::max<double, int, long double>(7.2, 4);
cout << a << " " << b << endl;
cout << c << " " << d << endl;
}
重载函数模板
这个跟普通函数重载也类似:
#include <iostream>
using namespace std;
template <typename T>
T Max(T a, T b)
{
std::cout << "Max<T,T>()\n";
return b < a ? a : b;
}
#if 1
template <typename T>
T Max(T a, T b, T c)
{
cout << "Max<T,T,T>()" << endl;
return Max(Max(a, b), c);
}
#endif // !1
template <typename T>
T Max(T *a, T *b)
{
cout << "Max<T*>()\n";
return (*b) < (*a) ? (*a) : (*b);
}
int main()
{
/// @brief 函数模板的重载
/// @return
cout << Max<>(15, 5) << endl;
int a = 55, b = 99;
cout << Max<>(&a, &b) << endl;
cout << Max<>(25, 23, 78) << endl;
}
ps. 由于函数模板重载,所以函数模板并不像类模板一样可以进行偏特化。
还有两点关于重载的基本原则需要了解一下:
重载时最好不要随便改变模板参数个数
,最好可以显示的指定模板参数类型
模板函数特化
有时通用的函数模板不能解决个别类型的问题,我们必须对此进行定制,这就是函数模板的特化。函数模板的特化必须把所有的模版参数全部指定。
#include <iostream>
using namespace std;
/// @brief 函数模板的全特化
/// @param argc
/// @param argv
/// @return
namespace internal
{
template <typename T, typename U>
auto func(T arg, U arg2)
{
return arg + arg2;
}
template <>
auto func(string x, string y)
{
return x + y;
}
/// 函数模板的全特化是根据函数模板来的,所以参数不能多或少(但类型可以变化),
// 但是函数体是可以改变的
// template <>
// auto func(string x, string y, string z)
// {
// return x + y + z;
// }
// template<>
// auto func(string x)
// {
// return x ;
// }
auto func(string x, int y)
{
return new string(x, y);
}
auto func(int x, int y)
{
return new string(x, y);
}
}
int main(int argc, char **argv)
{
cout << internal::func(20, 30) << endl;
cout << internal::func("test", 30) << endl;
cout << internal::func(string("test"), string("Code"));
}
非类型模板参数
因为T前边有一个typename/calss ,这表示T代表一个类型,是一个类型参数。
那么在模板参数列表里边,还可以定义非类型参数;非类型参数代表的是一个值。
既然非类型参数代表一个值,那么我们肯定不能用typename/class这种关键字来修饰这个值。
我们当然要用以往学习过的传统类型名来指定费类型参数。比如你非类型参数S,如果是个整型,那么就用 int s。
当模板被实例化时,这种非类型模板参数的值,或者是用户提供的,或者是编译器推断的,都有可能。
但是这些值都必须是常量表达式。因为实例化这些模板实在编译器编译时进行的。
template <int a, int b>
int func2()
{
int he = a + b;
return he;
}
int result = func2<2, 3>(); //显示指定模板参数——用<>提供额外信息
int i = 12;
int result2 = func2<i, 3>(); //不可以,报错,必须给定编译时就能确定的值这里i是在运行 时才能确定的值
实例化模板实在编译时进行的不是运行时
template <typename T, int a, int b>
int func3(T c)
{
int result = (int)c + a + b;
return result;
}
int i = func3<int, 11, 12>(13);
int i = func3<double, 11, 12>(13); //这里系统会以<>传递的类型为准,将13转成double型
这里 L1与L2的值是编译器推断出来的
template <unsigned L1, unsigned L2>
int charscomp(const char (&p1)[L1], const char (&p2)[L2])
{
return strcmp(p1, p2);
}
int result3 = charscomp("test1","test"); //没有提供费类型模板参数,系统会根据test1
//的长度6个,test的长度5个,来取代L1,L2
模板函数可以是内联函数
template <typename T, int a, int b>
inline
int func3(T c)
{
int result = (int)c + a + b;
return result;
}
模板定义并不会导致编译器生成代码,只有在我们调用这个函数模板时,使用编译器为我们实例化了一个特定版本的函数之后编译器才会生成代码。
编译器生成代码的时候,需要能够找到函数的函数体,所以,函数模板的定义通常都是在 .h 中
这里 L1与L2的值是编译器推断出来的
template <unsigned L1, unsigned L2>
int charscomp(const char (&p1)[L1], const char (&p2)[L2])
{
return strcmp(p1, p2);
}
int result3 = charscomp("test1","test"); //没有提供费类型模板参数,系统会根据test1
//的长度6个,test的长度5个,来取代L1,L2
模板函数可以是内联函数
template <typename T, int a, int b>
inline
int func3(T c)
{
int result = (int)c + a + b;
return result;
}
模板定义并不会导致编译器生成代码,只有在我们调用这个函数模板时,使用编译器为我们实例化了一个特定版本的函数之后编译器才会生成代码。
编译器生成代码的时候,需要能够找到函数的函数体,所以,函数模板的定义通常都是在 .h 中