函数模版
我们可以使用template
关键字引入模版,比如
template <typename T>
void fun(T input)
{
}
typename
关键字可以替换为class
,含义相同
函数模版中包含了两对参数:函数形参/实参;模版形参/实参
函数模版需要进行显式实例化,比如
fun<int>(3);
关于显式实例化,需要知道以下几点:
- 实例化会使得编译器产生相应的函数(函数模版并非函数,不能调用)
- 模版函数在编译期的两阶段处理
- 模版语法检查
- 模版实例化
- 模版必须在实例化时可见->模版的翻译单元级的一处定义原则(不同于普通函数的程序级的一处定义原则)
- 注意函数模版与内联函数的区别
接下来我们讨论一个概念-函数模版的重载
template <typename T>
void fun(T)
{
}
template <typename T, typename T2>
void fun(T input1, T2 input2)
{
}
接下来我们来讨论模版实参的类型推导,主要分为以下几部分
- 如果函数模版在实例化时没有显式制定模版实参,那么系统会尝试进行推导,推导是基于函数实参(表达式)确定模版实参的过程,其基本原则与
auto
类型推导类似,主要有以下几条规则:-
函数形参是左值引用/指针:
- 忽略表达式类型中的引用
- 将表达式类型与函数形参进行模式匹配以确定模版实参
template <typename T> void fun(T& input){ std::cout << input << std::endl; } int main() { int y = 3; int& x = y; fun(x); }
-
函数形参是万能引用:
- 如果实参表达式是右值,那么模版形参被推导为去掉引用的基本类型
- 若果实参表达式是左值,那么模版形参被推导为左值引用,触发引用折叠
template <typename T> void fun(T&& input){ std::cout << input << std::endl; } int main() { fun(3); // -> int && int x = 3; fun(x); // -> int & && -> int & }
-
函数形参不包含引用:
- 忽略表达式中的引用
- 忽略顶层
const
- 数组、函数转换成对应的指针类型
template <typename T> void fun(T input){ std::cout << input << std::endl; } int main() { fun(3); // -> int int x = 3; int & ref1 = x; fun(ref1); // -> int const int & ref2 = x; fun(ref2); // -> int const int * const ptr = &x; fun(ptr); // -> const int * int y[3]; fun(y); // -> int * }
-
关于模版实参的类型推导,我们需要注意以下几点
-
模版实参并非总能推导得到
- 如果模版形参和函数形参无关,则无法推导
template <typename T,typename Res> Res fun(T input){ } int main() { fun(3); }
- 即使相关,也不一定能进行推导,推导成功也可能因存在歧义而无法使用
template <typename T> void fun(unsigned input = sizeof(T)){ } int main() { fun(3); // 无法推导 }
template <typename T> void fun(T t1, T t2){ } int main() { fun(3,5.0); // 存在冲突 }
- 如果模版形参和函数形参无关,则无法推导
-
在无法推导时,编译器会选择使用缺省模版实参
template <typename T = int> void fun(unsigned x = sizeof(T)){ } int main() { fun(3); }
我们可以为任意位置的模版形参指定缺省模版实参,这里需要注意与函数缺省实参的区别
-
我们可以显式指定部分模版实参
- 显式指定的模版实参必须从最左边开始,依次指定
- 模版形参的声明顺序会影响调用的灵活性
template <typename T, typename Res> Res fun1(T input){ } template <typename Res, typename T> Res fun2(T input){ } int main() { fun1<int>(3); // 无法指定Res fun2<int>(3); // 可以指定Res }
-
函数模版自动推导时候会遇到一些特殊情况
- 函数形参无法匹配-
SFINAE
(替换失败并非错误) - 模版与非模版同时匹配,且匹配等级相同时,此时系统会选择非模版的版本
- 多个模版同时匹配,此时采用偏序关系确定选择“最特殊”的版本
- 函数形参无法匹配-
之后我们来关注函数模版的实例化控制,其中包括以下几个关键点
- 显式实例化定义:
template void fun<int>(int) / template void fun(int)
- 显式实例化声明:`extern template void fun(int) / extern template void fun(int)
- 注意显式实例化定义在程序级别的一处定义原则
- 注意实例化过程中的模版形参推导
接下来我们来关注与模版实例化非常相似的一个概念,函数模版的(完全)特化:template<> void f<int>(int) / template<> void f(int)
,其中包括以下几个关键点
- 函数模版的特化并不引入新的(同名)名称,只是为某个模版针对特定的模版实参提供优化的算法,需要注意与函数模版重载的区别
- 注意特化过程中的模版形参推导
虽然我们花时间讨论了函数模版的特化,但我们应该尽量避免使用函数模版的特化,主要原因有以下几点
- 函数模版的特化不参与重载解析,会产生反直觉的效果
template <typename T> void fun(T x){ std::cout << "1\n"; } template <typename T> void fun(T * x){ std::cout << "2\n"; } template<> void fun<int *>(int *x){ std::cout << "3\n"; } int main() { int x; fun(&x); // 输出2 }
- 通常函数模版特化可以使用重载代替
template <typename T> void fun(T x){ std::cout << "1\n"; } template <typename T> void fun(T * x){ std::cout << "2\n"; } void fun(int *x){ std::cout << "3\n"; } int main() { int x; fun(&x); // 输出3 }
- 存在一些不便于重载的情况:无法建立模版形参与函数形参的关联
对于这种情况,我们有以下几种解决办法:template <typename T,typename Res> Res fun(T x){ return Res{}; // 无法使用函数重载来代替 }
-
使用
if constexpr
解决template <typename T,typename Res> Res fun(T x){ if constexpr (std::is_same_v<Res,int>){ } else{ } return Res{}; }
-
通过引入“假函数形参”来解决
template <typename T,typename Res> Res fun(T x, const Res &){ return Res{}; } template <typename T> int fun(T x, const int &){ return int{}; }
-
通过类模版特化来解决
-
在C++20中引入了函数模版的简化形式,可以使用auto
定义模版参数类型,但在函数内部需要间接获取参数类型信息
void fun(auto x){
decltype(x) tmp = 3;
}