#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(1, 2) << '\n'; // 实例化 max<int>
std::cout << max(1.5, 2.5) << '\n'; // 实例化 max<double>
return 0;
}
现在考虑下面看似相似的程序:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(2, 3.5) << '\n'; // 编译错误
}
你可能会惊讶地发现这段代码无法通过编译,编译器会输出一大堆(可能令人困惑的)错误信息。在 Visual Studio 上作者得到:
Project3.cpp(11,18): error C2672: 'max': 找不到匹配的重载函数
Project3.cpp(11,28): error C2782: 'T max(T,T)': 模板形参 'T' 存在歧义
...
在调用 max(2, 3.5)
时,我们传入了两种不同类型:一个 int
和一个 double
。由于未使用尖括号显式指定类型,编译器首先查找是否存在非模板匹配 max(int, double)
,未找到;随后尝试模板实参推导,同样失败,因为模板形参 T
只能代表单一类型,无法同时表示 int
和 double
。换言之,函数模板中两个形参都是 T
,必须解析为同一实际类型。
你可能疑惑为何编译器不生成 max<double>(double, double)
,再通过数值转换把 int
提升为 double
。答案是:模板实参推导阶段不进行类型转换;类型转换仅在重载决议时考虑。这种设计有意为之:
- 简化规则——要么完全匹配,要么失败;
- 允许我们强制要求多个形参类型相同。
解决方案
我们有三种解决方案。
方案一:用 static_cast
把实参转换为同一类型
std::cout << max(static_cast<double>(2), 3.5) << '\n';
现在两实参均为 double
,编译器实例化 max<double>(double, double)
。
缺点:调用端冗长、可读性差。
方案二:显式指定模板实参
std::cout << max<double>(2, 3.5) << '\n';
显式指定 T = double
,编译器直接实例化 max<double>
,然后把 int
隐式转换为 double
。
比方案一简洁,但仍需思考类型。
方案三:使用多模板形参
根源在于仅用了一个模板形参 T
。改用两个形参 T
和 U
:
#include <iostream>
template <typename T, typename U>
T max(T x, U y) // x 为 T,y 为 U
{
return (x < y) ? y : x; // 窄化转换警告
}
int main()
{
std::cout << max(2, 3.5) << '\n'; // 实例化 max<int, double>
}
此时 T=int
, U=double
,编译器乐意实例化 max<int, double>
。但返回类型被声明为 T=int
,导致 3.5
被窄化为 3
,结果错误。
关键洞察T
与 U
独立解析,可相同也可不同。
修正:返回类型推导
让编译器根据返回语句推导返回类型:
template <typename T, typename U>
auto max(T x, U y)
{
return (x < y) ? y : x;
}
auto
返回类型需完整定义可见(不能仅靠前向声明)。
高级:显式返回类型
若需前向声明,可用 std::common_type_t
:
#include <type_traits>
template <typename T, typename U>
auto max(T x, U y) -> std::common_type_t<T, U>;
C++20 缩写函数模板
C++20 允许用 auto
形参自动生成模板:
auto max(auto x, auto y)
{
return (x < y) ? y : x;
}
等价于:
template <typename T, typename U>
auto max(T x, U y) { ... }
若要求两形参同类型,则无简洁缩写形式。
最佳实践
C++20 及以后,若各形参应独立类型,优先使用缩写函数模板。
函数模板可重载
模板之间也可重载,只要模板形参个数或函数形参列表不同:
#include <iostream>
template <typename T>
auto add(T x, T y) { return x + y; }
template <typename T, typename U>
auto add(T x, U y) { return x + y; }
template <typename T, typename U, typename V>
auto add(T x, U y, V z) { return x + y + z; }
int main()
{
std::cout << add(1.2, 3.4) << '\n'; // 匹配 add<double>
std::cout << add(5.6, 7) << '\n'; // 匹配 add<double,int>
std::cout << add(8, 9, 10) << '\n'; // 匹配 add<int,int,int>
}
重载规则:编译器优先选择更受限/更特化的模板。
若无法确定谁更特化,则报二义性错误。