11-7 函数模板实例化

在上节课(11.6——函数模板)中,我们介绍了函数模板,并将普通的max()函数转换为max函数模板:

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

本节将重点探讨函数模板的实际应用场景。


函数模板的使用

函数模板本身并非实际函数——其代码不会直接编译或执行。函数模板的唯一作用是生成可编译执行的函数。

要使用 max 函数模板,可通过以下语法进行函数调用:

max<actual_type>(arg1, arg2); // actual_type is some actual type, like int or double

这与普通函数调用极为相似——主要区别在于尖括号内添加的类型(称为模板实参template argument),该参数指定了将替代模板类型 T 的实际类型。

通过简单示例说明:

#include <iostream>

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

int main()
{
    std::cout << max<int>(1, 2) << '\n'; // instantiates and calls function max<int>(int, int)

    return 0;
}

当编译器遇到函数调用 max(1, 2) 时,它将判定 max(int, int) 的函数定义尚未存在。因此,编译器将隐式使用我们的 max 函数模板来创建该定义。

将函数模板(含模板类型)转换为具体类型函数的过程称为函数模板实例化function template instantiation(简称实例化instantiation)。因函数调用而触发的实例化称为隐式实例化implicit instantiation。从模板实例化的函数在技术上称为特化specialization,但通常被称为函数实例function instance。产生特化的原始模板称为主模板primary template。函数实例在所有方面都与普通函数无异。

术语说明
“特化”一词更常用于指显式特化,即允许我们显式定义特化(而非由主模板隐式实例化)。显式特化将在第26.3节——函数模板特化中讲解。
函数实例化的过程很简单:编译器本质上克隆了原始模板,并将模板类型(T)替换为我们指定的实际类型(int)。

函数实例化的过程很简单:编译器本质上克隆了原始模板,并将模板类型(T)替换为我们指定的实际类型(int)。

因此当我们调用 max(1, 2) 时,实例化的函数特化形大致如下所示:

template<> // ignore this for now
int max<int>(int x, int y) // the generated function max<int>(int, int)
{
    return (x < y) ? y : x;
}

以下是与上文相同的示例,展示在完成所有实例化后编译器实际编译的内容:

#include <iostream>

// a declaration for our function template (we don't need the definition any more)
template <typename T>
T max(T x, T y);

template<>
int max<int>(int x, int y) // the generated function max<int>(int, int)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n'; // instantiates and calls function max<int>(int, int)

    return 0;
}

你可以自己编译这个代码并验证其正确性。函数模板仅在每个翻译单元中首次调用时才会实例化。后续对该函数的调用将被路由至已实例化的函数。

反之,若函数模板未被调用,则该翻译单元中不会进行实例化。

再看另一个示例:

#include <iostream>

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

int main()
{
    std::cout << max<int>(1, 2) << '\n';    // instantiates and calls function max<int>(int, int)
    std::cout << max<int>(4, 3) << '\n';    // calls already instantiated function max<int>(int, int)
    std::cout << max<double>(1, 2) << '\n'; // instantiates and calls function max<double>(double, double)

    return 0;
}

这与前一个示例类似,但这次我们的函数模板将用于生成两个函数:一次将T替换为int,另一次将T替换为double。完成所有实例化后,程序将呈现如下形态:

#include <iostream>

// a declaration for our function template (we don't need the definition any more)
template <typename T>
T max(T x, T y);

template<>
int max<int>(int x, int y) // the generated function max<int>(int, int)
{
    return (x < y) ? y : x;
}

template<>
double max<double>(double x, double y) // the generated function max<double>(double, double)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n';    // instantiates and calls function max<int>(int, int)
    std::cout << max<int>(4, 3) << '\n';    // calls already instantiated function max<int>(int, int)
    std::cout << max<double>(1, 2) << '\n'; // instantiates and calls function max<double>(double, double)

    return 0;
}

需特别注意:实例化max时,生成的函数形参类型为double。由于我们提供了int实参,这些参数将被隐式转换为double类型。


模板实参推导

多数情况下,实例化所需的实际类型与函数形参类型一致。例如:

std::cout << max<int>(1, 2) << '\n'; // specifying we want to call max<int>

此函数调用中,我们既指定用 int 替换 T,又实际传入了 int 实参。

当实参类型与实际所需类型一致时,无需显式指定类型——可借助模板实参推导template argument deduction机制,让编译器根据函数调用中的实参类型推导出应使用的实际类型。

std::cout << max<int>(1, 2) << '\n'; // specifying we want to call max<int>

我们可以采用以下任一形式:

std::cout << max<>(1, 2) << '\n';
std::cout << max(1, 2) << '\n';

无论哪种情况,编译器都会检测到未提供实际类型,因此会尝试从函数实参推导出实际类型,从而生成所有模板形参均与传入实参类型匹配的 max() 函数。在此示例中,编译器将推断出使用函数模板 max 时,实际类型为 int 可实例化函数 max(int),从而使两个函数形参的类型(int)与传入实参的类型(int)相匹配。

两种情况的差异在于编译器解析重载函数调用的机制:顶部案例(带空尖括号)中,编译器仅考虑 max 模板函数重载项;底部案例(无尖括号)中,编译器将同时考虑 max 模板函数重载项与 max 非模板函数重载项。在下方情况(无尖括号)中,编译器将同时考虑 max 模板函数重载与 max 非模板函数重载。当下方情况导致模板函数与非模板函数均具备同等可行性时,编译器将优先选择非模板函数。

关键洞察
常规函数调用语法会优先选择非模板函数,而非同等可行的模板函数实例化形式。

例如:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    std::cout << "called max<int>(int, int)\n";
    return (x < y) ? y : x;
}

int max(int x, int y)
{
    std::cout << "called max(int, int)\n";
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n'; // calls max<int>(int, int)
    std::cout << max<>(1, 2) << '\n';    // deduces max<int>(int, int) (non-template functions not considered)
    std::cout << max(1, 2) << '\n';      // calls max(int, int)

    return 0;
}

image

请注意底部案例中的语法与普通函数调用完全一致!在大多数情况下,这种普通函数调用语法正是我们用于调用函数模板实例化函数的方式。

存在这种情况有几个原因:

  • 语法更为简洁。
  • 同时存在匹配的非模板函数和函数模板的情况较为罕见。
  • 若确实存在匹配的非模板函数和函数模板,通常会优先调用非模板函数。

最后一点可能不太明显。函数模板的实现适用于多种类型——但因此必须保持泛化特性。而非模板函数仅处理特定类型的组合,其实现相较于函数模板版本,可能针对特定类型进行更优化的专属设计。例如:

#include <iostream>

// This function template can handle many types, so its implementation is generic
template <typename T>
void print(T x)
{
    std::cout << x; // print T however it normally prints
}

// This function only needs to consider how to print a bool, so it can specialize how it handles
// printing of a bool
void print(bool x)
{
    std::cout << std::boolalpha << x; // print bool as true or false, not 1 or 0
}

int main()
{
    print<bool>(true); // calls print<bool>(bool) -- prints 1
    std::cout << '\n';

    print<>(true);     // deduces print<bool>(bool) (non-template functions not considered) -- prints 1
    std::cout << '\n';

    print(true);       // calls print(bool) -- prints true
    std::cout << '\n';

    return 0;
}

最佳实践
调用由函数模板实例化的函数时,应优先使用常规函数调用语法(除非需要使函数模板版本优先于匹配的非模板函数)。


带非模板参数的函数模板

可以创建同时包含模板参数和非模板参数的函数模板。类型模板参数可匹配任何类型,而非模板参数则像普通函数的参数一样工作。

例如:

// T is a type template parameter
// double is a non-template parameter
// We don't need to provide names for these parameters since they aren't used
template <typename T>
int someFcn(T, double)
{
    return 5;
}

int main()
{
    someFcn(1, 3.4); // matches someFcn(int, double)
    someFcn(1, 3.4f); // matches someFcn(int, double) -- the float is promoted to a double
    someFcn(1.2, 3.4); // matches someFcn(double, double)
    someFcn(1.2f, 3.4); // matches someFcn(float, double)
    someFcn(1.2f, 3.4f); // matches someFcn(float, double) -- the float is promoted to a double

    return 0;
}

此函数模板具有一个模板化的第一个参数,但第二个参数类型为double且固定。请注意返回类型也可为任意类型。在此示例中,函数始终返回int值。


实例化函数可能无法始终编译通过

请考虑以下程序:

#include <iostream>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::cout << addOne(1) << '\n';
    std::cout << addOne(2.3) << '\n';

    return 0;
}

编译器将有效地编译并执行以下内容:

#include <iostream>

template <typename T>
T addOne(T x);

template<>
int addOne<int>(int x)
{
    return x + 1;
}

template<>
double addOne<double>(double x)
{
    return x + 1;
}

int main()
{
    std::cout << addOne(1) << '\n';   // calls addOne<int>(int)
    std::cout << addOne(2.3) << '\n'; // calls addOne<double>(double)

    return 0;
}

这将产生以下结果:

image

但如果我们尝试这样做呢?

#include <iostream>
#include <string>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::string hello { "Hello, world!" };
    std::cout << addOne(hello) << '\n';

    return 0;
}

当编译器尝试解析 addOne(hello) 时,它不会找到 addOne(std::string) 的非模板函数匹配项,但会找到我们为 addOne(T) 定义的函数模板,并确定可以由此生成 addOne(std::string) 函数。因此,编译器将生成并编译以下内容:

#include <iostream>
#include <string>

template <typename T>
T addOne(T x);

template<>
std::string addOne<std::string>(std::string x)
{
    return x + 1;
}

int main()
{
    std::string hello{ "Hello, world!" };
    std::cout << addOne(hello) << '\n';

    return 0;
}

image

然而,这将引发编译错误,因为当x是std::string时,x + 1没有意义。显而易见的解决方案就是避免将std::string类型的实参传递给addOne()函数。


实例化的函数在语义上未必总是合理

只要语法上合理,编译器就能成功编译实例化的函数模板。然而,编译器无法验证此类函数在语义上是否真正合理。

例如:

#include <iostream>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::cout << addOne("Hello, world!") << '\n';

    return 0;
}

在此示例中,我们对C风格字符串字面量调用了addOne()方法。这在语义上究竟意味着什么?谁知道呢!

或许令人惊讶的是,由于C++在语法上允许将整数值与字符串常量相加(我们将在后续第17.9节——指针运算与下标操作中讲解此内容),上述示例能够编译通过,并产生如下结果:

image

警告
只要语法上有效,编译器就会实例化并编译语义上不合理的函数模板。确保调用此类函数模板时传入的参数具有合理性,这是你的责任。

对于进阶读者

我们可以告知编译器,禁止使用特定参数实例化函数模板。这通过函数模板特化实现——它允许我们针对特定模板参数集重载函数模板,同时配合使用= delete,告知编译器任何使用该函数的行为都应触发编译错误。

#include <iostream>
#include <string>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

// Use function template specialization to tell the compiler that addOne(const char*) should emit a compilation error
// const char* will match a string literal
template <>
const char* addOne(const char* x) = delete;

int main()
{
    std::cout << addOne("Hello, world!") << '\n'; // compile error

    return 0;
}

我们在第26.3节——函数模板特化中介绍了函数模板特化。


函数模板与非模板形参的默认实参

与普通函数类似,函数模板可以为非模板形参设置默认实参。从模板实例化的每个函数都将使用相同的默认实参。

例如:

#include <iostream>

template <typename T>
void print(T val, int times=1)
{
    while (times--)
    {
        std::cout << val;
    }
}

int main()
{
    print(5);      // print 5 1 time
    print('a', 3); // print 'a' 3 times

    return 0;
}

这将输出:

image


谨防包含可修改静态局部变量的函数模板

第7.11节——静态局部变量中,我们讨论了静态局部变量,这类局部变量具有静态作用域(其生命周期与程序生命周期相同)。

当函数模板中使用静态局部变量时,每个从该模板实例化的函数都会拥有独立的静态局部变量实例。若该静态局部变量为const类型,通常不会引发问题。但若该静态局部变量可被修改,则可能导致结果与预期不符。

例如:

#include <iostream>

// Here's a function template with a static local variable that is modified
template <typename T>
void printIDAndValue(T value)
{
    static int id{ 0 };
    std::cout << ++id << ") " << value << '\n';
}

int main()
{
    printIDAndValue(12);
    printIDAndValue(13);

    printIDAndValue(14.5);

    return 0;
}

这产生了以下结果:

image

您可能预期最后一行会输出 3) 14.5。然而,编译器实际编译并执行的内容如下:

#include <iostream>

template <typename T>
void printIDAndValue(T value);

template <>
void printIDAndValue<int>(int value)
{
    static int id{ 0 };
    std::cout << ++id << ") " << value << '\n';
}

template <>
void printIDAndValue<double>(double value)
{
    static int id{ 0 };
    std::cout << ++id << ") " << value << '\n';
}

int main()
{
    printIDAndValue(12);   // calls printIDAndValue<int>()
    printIDAndValue(13);   // calls printIDAndValue<int>()

    printIDAndValue(14.5); // calls printIDAndValue<double>()

    return 0;
}

请注意,printIDAndValue 和 printIDAndValue 各有独立的静态局部变量 id,而非共享同一个变量。


泛型编程

由于模板类型可被任意实际类型替换,模板类型有时被称为泛型类型generic types。由于模板的编写可独立于具体类型,基于模板的编程有时被称为泛型编程generic programming。相较于C++通常强调类型与类型检查,泛型编程让我们能够专注于算法逻辑和数据结构设计,而无需过度关注类型信息。


结论

一旦习惯了编写函数模板,你会发现它们的编写时间其实并不比使用具体类型的函数长多少。函数模板通过最大限度地减少需要编写和维护的代码量,能显著降低代码维护成本并减少错误。

函数模板确实存在若干缺点,我们必须指出这些问题。首先,编译器会为每次具有独特实参类型的函数调用创建(并编译)独立函数。因此尽管函数模板编写简洁,但可能膨胀为海量代码,导致代码臃肿和编译速度变慢。函数模板更大的弊端在于其产生的错误信息往往混乱难辨,甚至接近不可读,远比普通函数的错误信息更难解读。这些错误信息可能令人望而生畏,但一旦理解其提示内容,其中指出的问题通常都相当容易解决。

相较于模板为编程工具箱带来的强大功能和安全性,这些缺点微不足道,因此在需要类型灵活性的任何场景中都应大胆使用模板!一个实用技巧是:先创建普通函数,若发现需要为不同形参类型重载,再将其转换为函数模板。

最佳实践
当需要处理多种类型时,请使用函数模板编写通用的泛型代码。

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