11-8 具有多种模板类型的函数模板

第11.6课——函数模板中,我们编写了一个函数模板来计算两个值的最大值:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max(1, 2) << '\n';   // will instantiate max(int, int)
    std::cout << max(1.5, 2.5) << '\n'; // will instantiate max(double, 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';  // compile error

    return 0;
}

你可能会惊讶地发现该程序无法编译。编译器将输出大量(可能看起来很奇怪的)错误信息。在 Visual Studio 中,作者得到如下提示:

Project3.cpp(11,18): error C2672: 'max': no matching overloaded function found
Project3.cpp(11,28): error C2782: 'T max(T,T)': template parameter 'T' is ambiguous
Project3.cpp(4): message : see declaration of 'max'
Project3.cpp(11,28): message : could be 'double'
Project3.cpp(11,28): message : or       'int'
Project3.cpp(11,28): error C2784: 'T max(T,T)': could not deduce template argument for 'T' from 'double'
Project3.cpp(4): message : see declaration of 'max'

在我的clang里面, 提示则如下:

image

在函数调用 max(2, 3.5) 中,我们传递了两种不同类型的参数:一个 int 和一个 double。由于未使用尖括号指定具体类型,编译器首先会查找是否存在 max(int, double) 的非模板匹配版本——但找不到。

接着编译器会尝试通过模板形参推导(详见第11.7节——函数模板实例化)寻找函数模板匹配。然而此过程同样会失败,原因很简单:T只能表示单一类型。不存在能让编译器将函数模板max(T, T)实例化为两个不同形参类型的函数的T类型。换言之,由于函数模板中两个形参均为T类型,它们必须解析为相同的实际类型。

由于既找不到非模板匹配也找不到模板匹配,函数调用无法解析,最终导致编译错误。

你可能会疑惑:为何编译器不生成函数 max(double, double),再通过数值转换将 int 实参转换为 double?答案很简单:类型转换仅在解析函数重载时发生,而在模板形参推导过程中不会进行。

这种类型转换缺失至少出于两个原因而刻意设计:首先,它有助于保持简单性——函数调用实参与模板类型形参要么完全匹配,要么完全不匹配。其次,它允许我们为需要确保两个或多个形参类型一致的情况创建函数模板(如上例所示)。

我们需要另寻解决方案。幸运的是,至少有三种方法可解决此问题。


使用static_cast将实参转换为匹配类型

第一种方案是让调用方承担将实参转换为匹配类型的责任。例如:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max(static_cast<double>(2), 3.5) << '\n'; // convert our int to a double so we can call max(double, double)

    return 0;
}

image

现在两个实参均为double类型,编译器就能实例化max(double, double)以满足此函数调用。

但此方案既笨拙又难读。


提供显式模板类型实参

若编写非模板的 max(double, double) 函数,则可调用 max(int, double),让隐式类型转换规则将 int 实参转换为 double,从而解析函数调用:

#include <iostream>

double max(double x, double y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max(2, 3.5) << '\n'; // the int argument will be converted to a double

    return 0;
}

然而,当编译器进行模板实参推导时,它不会执行任何类型转换。幸运的是,如果我们明确指定要使用的类型模板实参,就无需依赖模板实参推导:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

int main()
{
    // we've explicitly specified type double, so the compiler won't use template argument deduction
    std::cout << max<double>(2, 3.5) << '\n';

    return 0;
}

在上例中,我们调用了 max(2, 3.5)。由于显式指定 T 应替换为 double,编译器不会使用模板实参推导,而是直接实例化函数 max(double, double),并对任何不匹配的实参进行类型转换。我们的 int 形参将被隐式转换为 double。

虽然这种写法比使用static_cast更易读,但若能在调用max函数时完全无需考虑类型问题,效果会更理想。


具有多个模板类型形参的函数模板

问题的根源在于:我们仅为函数模板定义了单一模板类型(T),并要求两个形参都必须是该类型。

解决此问题的最佳方法是重写函数模板,使形参能够解析为不同类型。我们将不再使用单个模板类型形参 T,而是改用两个形参(T 和 U):

#include <iostream>

template <typename T, typename U> // We're using two template type parameters named T and U
T max(T x, U y) // x can resolve to type T, and y can resolve to type U
{
    return (x < y) ? y : x; // uh oh, we have a narrowing conversion problem here
}

int main()
{
    std::cout << max(2, 3.5) << '\n'; // resolves to max<int, double>

    return 0;
}

由于我们用模板类型 T 定义了 x,用模板类型 U 定义了 y,现在 x 和 y 可以独立解析其类型。当我们调用 max(2, 3.5) 时,T 可以是 int,U 可以是 double。编译器会愉快地为我们实例化 max<int, double>(int, double)。

关键洞察
由于 T 和 U 是独立的模板形参,它们各自独立地解析其类型。这意味着 T 和 U 可能解析为不同的类型,也可能解析为相同的类型。

然而,这个示例并不能正常工作。如果你编译并运行该程序(关闭“将警告视为错误”选项),它将产生以下结果:

image
image

这里发生了什么?2和3.5的最大值怎么可能是3?

条件运算符(?:)要求其(非条件)操作数具有相同的公共类型。通常采用算术规则(10.5节——算术转换)来确定公共类型,条件运算符的结果也将使用该公共类型。例如,int与double的共同类型是double,因此当条件运算符的(非条件)操作数为int和double时,运算结果将为double类型。本例中该值为3.5,结果正确。

然而函数声明的返回类型为T。当T为int而U为double时,函数返回类型即为int。此时3.5将经历向int类型的窄化转换,最终得到值3,导致数据丢失(并可能触发编译器警告)。

那么我们该如何解决这个问题?将返回类型改为U也无法解决问题,因为max(3.5, 2)中U作为int类型仍会出现相同的问题。

在这种情况下,返回类型推导(通过auto实现)就派上用场了——我们将让编译器根据返回语句推导出应有的返回类型:

#include <iostream>

template <typename T, typename U>
auto max(T x, U y) // ask compiler can figure out what the relevant return type is
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max(2, 3.5) << '\n';

    return 0;
}

image

此版本的max现已能正常处理不同类型的操作数。需注意的是,具有auto返回类型的函数必须在使用前完成完整定义(仅前向声明不足以满足要求),因为编译器需要检查函数实现才能确定其返回类型。

对于进阶读者

若需实现可前向声明的函数,必须显式指定返回类型。由于返回类型需为 T 和 U 的公共类型,可使用 std::common_type_t(详见第 10.5 节——算术转换)获取 T 和 U 的公共类型作为显式返回类型:

#include <iostream>
#include <type_traits> // for std::common_type_t

template <typename T, typename U>
auto max(T x, U y) -> std::common_type_t<T, U>; // returns the common type of T and U

int main()
{
    std::cout << max(2, 3.5) << '\n';

    return 0;
}

template <typename T, typename U>
auto max(T x, U y) -> std::common_type_t<T, U>
{
    return (x < y) ? y : x;
}

简写函数模板(C++20)

C++20引入了auto关键字的新用法:当auto关键字作为普通函数的形参类型时,编译器会自动将该函数转换为函数模板,每个auto形参均成为独立的模板类型形参。这种创建函数模板的方法称为简写函数模板abbreviated function template

例如:

auto max(auto x, auto y)
{
    return (x < y) ? y : x;
}

在 C++20 中是以下代码的简写形式:

template <typename T, typename U>
auto max(T x, U y)
{
    return (x < y) ? y : x;
}

这与我们上面编写的max函数模板相同。

当需要每个模板类型形参成为独立类型时,这种形式更受青睐——移除模板形参声明行能使代码更简洁易读。

若需多个auto形参采用相同类型,目前尚无简洁的缩写函数模板可用。也就是说,对于如下场景尚无便捷的缩写函数模板:

template <typename T>
T max(T x, T y) // two parameters of the same type
{
    return (x < y) ? y : x;
}

最佳实践
请随意使用带单个auto参数的简写函数模板,或在每个auto参数应为独立类型的情境下使用(前提是您的语言标准已设置为C++20或更高版本)。


函数模板可以重载

正如函数可以重载,函数模板同样可以重载。此类重载可以具有不同的模板类型数量和/或不同的函数形参数量或类型:

#include <iostream>

// Add two values with matching types
template <typename T>
auto add(T x, T y)
{
    return x + y;
}

// Add two values with non-matching types
// As of C++20 we could also use auto add(auto x, auto y)
template <typename T, typename U>
auto add(T x, U y)
{
    return x + y;
}

// Add three values with any type
// As of C++20 we could also use auto add(auto x, auto y, auto z)
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'; // instantiates and calls add<double>()
    std::cout << add(5.6, 7) << '\n';   // instantiates and calls add<double, int>()
    std::cout << add(8, 9, 10) << '\n'; // instantiates and calls add<int, int, int>()

    return 0;
}

值得注意的是,对于调用 add(1.2, 3.4) 的情况,编译器会优先选择 add(T, T) 而非 add<T, U>(T, U),尽管两者都可能匹配。

确定多个匹配函数模板中应优先使用哪一个的规则称为“函数模板的偏序关系”。简而言之,更严格/更特化的函数模板将被优先采用。在此例中,add(T, T) 因仅含一个模板形参而成为更严格的函数模板,故被优先选用。

若多个函数模板均可匹配调用且编译器无法判定其严格程度,则会报错提示匹配模糊。

posted @ 2026-03-07 14:21  游翔  阅读(1)  评论(0)    收藏  举报