13-13 类模板

在第11.6节——函数模板中,我们介绍了这样一个难题:对于每种不同的类型组合,都需要单独创建一个(重载的)函数:

#include <iostream>

// function to calculate the greater of two int values
int max(int x, int y)
{
    return (x < y) ? y : x;
}

// almost identical function to calculate the greater of two double values
// the only difference is the type information
double max(double x, double y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max(5, 6);     // calls max(int, int)
    std::cout << '\n';
    std::cout << max(1.2, 3.4); // calls max(double, double)

    return 0;
}

image

解决此问题的方案是创建一个函数模板,编译器可利用该模板为所需的任意类型集合实例化常规函数:

#include <iostream>

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

int main()
{
    std::cout << max(5, 6);     // instantiates and calls max<int>(int, int)
    std::cout << '\n';
    std::cout << max(1.2, 3.4); // instantiates and calls max<double>(double, double)

    return 0;
}

image

相关内容:
我们在第11.7节——函数模板实例化中介绍了函数模板实例化的工作原理。


聚合类型面临类似的挑战

在处理聚合类型(包括结构体/类/联合体和数组)时,我们同样会遇到类似的挑战。

例如,假设我们正在编写一个需要处理整数对的程序,并需要判断两个数中哪个更大。我们可能会编写如下程序:

#include <iostream>

struct Pair
{
    int first{};
    int second{};
};

constexpr int max(Pair p) // pass by value because Pair is small
{
    return (p.first < p.second ? p.second : p.first);
}

int main()
{
    Pair p1{ 5, 6 };
    std::cout << max(p1) << " is larger\n";

    return 0;
}

image

后来,我们发现还需要成对的双精度数值。因此,我们将程序更新为如下内容:

#include <iostream>

struct Pair
{
    int first{};
    int second{};
};

struct Pair // compile error: erroneous redefinition of Pair
{
    double first{};
    double second{};
};

constexpr int max(Pair p)
{
    return (p.first < p.second ? p.second : p.first);
}

constexpr double max(Pair p) // compile error: overloaded function differs only by return type
{
    return (p.first < p.second ? p.second : p.first);
}

int main()
{
    Pair p1{ 5, 6 };
    std::cout << max(p1) << " is larger\n";

    Pair p2{ 1.2, 3.4 };
    std::cout << max(p2) << " is larger\n";

    return 0;
}

image
image
image
image

遗憾的是,该程序无法编译,存在若干需要解决的问题。

首先,与函数不同,类型定义无法重载。编译器会将Pair的第二次定义视为对首次定义的错误重申。其次,尽管函数支持重载,但我们的max(Pair)函数仅在返回类型上存在差异,而重载函数不能仅凭返回类型进行区分。第三,这里存在大量冗余。每个Pair结构体完全相同(除数据类型外),max(Pair)函数也完全相同(除返回类型外)。

前两个问题可通过为Pair结构体命名差异化(如PairInt和PairDouble)解决。但这既需要记忆命名规则,又需为每种新增配对类型克隆大量代码,本质上未能消除冗余。

所幸我们有更优解。

作者注:
若您对函数模板、模板类型或函数模板实例化的工作原理尚不明确,请在继续学习前复习第11.6节——函数模板与第11.7节——函数模板实例化。


类模板

正如函数模板是用于实例化函数的模板定义,类模板class template则是用于实例化l类类型的模板定义。

提醒:
“类类型class type”指结构体、类或联合体类型。为简化说明,本文将通过结构体演示“类模板class templates”,但所有内容同样适用于类。

作为提醒,以下是我们 int 类型 pair 结构体的定义:

struct Pair
{
    int first{};
    int second{};
};

让我们将配对类重写为类模板

#include <iostream>

template <typename T>
struct Pair
{
    T first{};
    T second{};
};

int main()
{
    Pair<int> p1{ 5, 6 };        // instantiates Pair<int> and creates object p1
    std::cout << p1.first << ' ' << p1.second << '\n';

    Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
    std::cout << p2.first << ' ' << p2.second << '\n';

    Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
    std::cout << p3.first << ' ' << p3.second << '\n';

    return 0;
}

image

与函数模板类似,类模板的定义也从模板参数声明开始。首先使用模板关键字,接着在尖括号(<>)内指定类模板所需的所有模板类型。每种模板类型前需使用关键字 typename(推荐)或 class(不推荐),后跟模板类型名称(如 T)。在此示例中,由于两个成员类型相同,我们仅需定义一种模板类型。

随后按常规方式定义结构体,区别在于可将模板类型(T)用于任何需要模板类型的位置——这些类型将在后续被具体类型替换。至此,类模板定义完成。

在 main 函数中,我们可以使用任意类型实例化 Pair 对象。首先实例化 Pair 类型对象。由于尚未存在 Pair 的类型定义,编译器会调用类模板实例化名为 Pair 的结构体类型定义,其中所有模板类型 T 均被替换为 int 类型。

接着实例化类型为 Pair 的对象,此时会实例化名为 Pair 的结构体类型定义,其中 T 被替换为 double。对于 p3,Pair 已被实例化,因此编译器将使用先前定义的类型。

以下是与上述相同的示例,展示所有模板实例化完成后编译器实

#include <iostream>

// A declaration for our Pair class template
// (we don't need the definition any more since it's not used)
template <typename T>
struct Pair;

// Explicitly define what Pair<int> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<int>
{
    int first{};
    int second{};
};

// Explicitly define what Pair<double> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<double>
{
    double first{};
    double second{};
};

int main()
{
    Pair<int> p1{ 5, 6 };        // instantiates Pair<int> and creates object p1
    std::cout << p1.first << ' ' << p1.second << '\n';

    Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
    std::cout << p2.first << ' ' << p2.second << '\n';

    Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
    std::cout << p3.first << ' ' << p3.second << '\n';

    return 0;
}

image

你可以直接编译这个示例,并看到它按预期运行!

面向高级读者:
上述示例运用了名为类模板特化的特性(将在后续第26.4节——类模板特化中讲解)。目前无需了解该特性的具体工作原理。


在函数中使用类模板

现在让我们回到让 max() 函数支持不同类型的挑战。由于编译器将 Pair 和 Pair 视为独立类型,我们可以使用通过参数类型区分的重载函数:

constexpr int max(Pair<int> p)
{
    return (p.first < p.second ? p.second : p.first);
}

constexpr double max(Pair<double> p) // okay: overloaded function differentiated by parameter type
{
    return (p.first < p.second ? p.second : p.first);
}

虽然这段代码能编译通过,但并未解决冗余问题。我们真正需要的是能接受任意类型元组的函数。换言之,我们需要一个接受类型为 Pair 的参数的函数,其中 T 是模板类型参数。这意味着我们需要使用函数模板来实现!

以下是完整示例,其中 max() 被实现为函数模板:

#include <iostream>

template <typename T>
struct Pair
{
    T first{};
    T second{};
};

template <typename T>
constexpr T max(Pair<T> p)
{
    return (p.first < p.second ? p.second : p.first);
}

int main()
{
    Pair<int> p1{ 5, 6 };
    std::cout << max<int>(p1) << " is larger\n"; // explicit call to max<int>

    Pair<double> p2{ 1.2, 3.4 };
    std::cout << max(p2) << " is larger\n"; // call to max<double> using template argument deduction (prefer this)

    return 0;
}

image

max()函数模板的设计相当直观。由于需要传入Pair参数,我们需要编译器理解T的具体类型。因此,函数开头必须声明模板参数声明来定义模板类型T。随后既可将T作为返回类型,也可作为Pair的模板类型。

当 max() 函数被 Pair 参数调用时,编译器会从函数模板中实例化函数 int max(Pair),此时模板类型 T 被替换为 int。以下代码片段展示了编译器在此情况下实际实例化的内容:

template <>
constexpr int max(Pair<int> p)
{
    return (p.first < p.second ? p.second : p.first);
}

与所有函数模板调用相同,我们可以显式指定模板类型参数(例如 max(p1)),也可以隐式调用(例如 max(p2)),让编译器通过模板参数推导来确定模板类型参数的具体值。


包含模板类型成员和非模板类型成员的类模板

类模板可以包含使用模板类型的成员,同时也包含使用普通(非模板)类型的成员。例如:

template <typename T>
struct Foo
{
    T first{};    // first will have whatever type T is replaced with
    int second{}; // second will always have type int, regardless of what type T is
};

这完全符合预期:第一个参数将是模板类型 T 的具体类型,第二个参数始终是 int 类型。


具有多种模板类型的类模板

类模板也可以包含多种模板类型。例如,若希望Pair类的两个成员能够具有不同类型,我们可以为Pair类模板定义两种模板类型:

#include <iostream>

template <typename T, typename U>
struct Pair
{
    T first{};
    U second{};
};

template <typename T, typename U>
void print(Pair<T, U> p)
{
    std::cout << '[' << p.first << ", " << p.second << ']';
}

int main()
{
    Pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double
    Pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
    Pair<int, int> p3{ 7, 8 };      // a pair holding two ints

    print(p2);

    return 0;
}

image

要定义多个模板类型,在模板参数声明中,我们用逗号分隔每个所需的模板类型。在上面的示例中,我们定义了两种不同的模板类型,一种名为 T,一种名为 U。T 和 U 的实际模板类型参数可以不同(如上面的 p1 和 p2 的情况)或相同(如 p3 的情况)。


使函数模板支持多种类类型

考虑上述示例中的 print() 函数模板:

template <typename T, typename U>
void print(Pair<T, U> p)
{
    std::cout << '[' << p.first << ", " << p.second << ']';
}

由于我们已将函数参数显式定义为 Pair<T, U>,因此只有类型为 Pair<T, U> 的参数(或可转换为 Pair<T, U> 的参数)才会匹配。若我们仅希望函数能接受 Pair<T, U> 参数调用,这种设计最为理想。

在某些情况下,我们可能需要编写适用于任何类型且能成功编译的函数模板。此时只需将类型模板参数作为函数参数即可实现。

例如:

#include <iostream>

template <typename T, typename U>
struct Pair
{
    T first{};
    U second{};
};

struct Point
{
    int first{};
    int second{};
};

template <typename T>
void print(T p) // type template parameter will match anything
{
    std::cout << '[' << p.first << ", " << p.second << ']'; // will only compile if type has first and second members
}

int main()
{
    Pair<double, int> p1{ 4.5, 6 };
    print(p1); // matches print(Pair<double, int>)

    std::cout << '\n';

    Point p2 { 7, 8 };
    print(p2); // matches print(Point)

    std::cout << '\n';

    return 0;
}

image

在上例中,我们重写了print()函数,使其仅包含单个类型模板参数(T),该参数可匹配任意类型。只要对象具有第一个和第二个成员,函数主体就能成功编译。我们通过调用print()函数来演示这一点:先传入类型为Pair<double, int>的对象,再传入类型为Point的对象。

存在一种可能产生误解的情况。请考虑以下版本的print():

template <typename T, typename U>
struct Pair // defines a class type named Pair
{
    T first{};
    U second{};
};

template <typename Pair> // defines a type template parameter named Pair (shadows Pair class type)
void print(Pair p)       // this refers to template parameter Pair, not class type Pair
{
    std::cout << '[' << p.first << ", " << p.second << ']';
}

你可能会认为该函数仅在传入Pair类类型参数时才会匹配。但此版本的print()在功能上与先前将模板参数命名为T的版本完全相同,可匹配任何类型。问题在于当我们将Pair定义为类型模板参数时,它会遮蔽全局作用域内其他对Pair名称的使用。因此在函数模板内部,Pair指代的是模板参数Pair,而非类类型Pair。由于类型模板参数会匹配任何类型,这个Pair便能匹配任意参数类型,而不仅限于类类型Pair!

这正是坚持使用简单模板参数名称(如T、U、N)的合理依据——这类名称较少会遮蔽类类型名称。

std::pair

由于成对数据的操作十分常见,C++标准库中包含一个名为std::pair的类模板(位于头文件中),其定义与前文所述的多模板类型Pair类模板完全一致。实际上,我们可以将自定义的pair结构体替换为std::pair:

#include <iostream>
#include <utility>

template <typename T, typename U>
void print(std::pair<T, U> p)
{
    // the members of std::pair have predefined names `first` and `second`
    std::cout << '[' << p.first << ", " << p.second << ']';
}

int main()
{
    std::pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double
    std::pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
    std::pair<int, int> p3{ 7, 8 };      // a pair holding two ints

    print(p2);

    return 0;
}

image

在本节课中,我们开发了自己的Pair类来演示其工作原理,但在实际编码中,应优先使用std::pair而非自行编写。


在多个文件中使用类模板

与函数模板类似,类模板通常定义在头文件中,以便被任何需要它们的代码文件包含。模板定义和类型定义均不受单定义规则限制,因此不会引发问题:

pair.h

#ifndef PAIR_H
#define PAIR_H

template <typename T>
struct Pair
{
    T first{};
    T second{};
};

template <typename T>
constexpr T max(Pair<T> p)
{
    return (p.first < p.second ? p.second : p.first);
}

#endif

foo.cpp:

#include "pair.h"
#include <iostream>

void foo()
{
    Pair<int> p1{ 1, 2 };
    std::cout << max(p1) << " is larger\n";
}

main.cpp:

#include "pair.h"
#include <iostream>

void foo(); // forward declaration for function foo()

int main()
{
    Pair<double> p2 { 3.4, 5.6 };
    std::cout << max(p2) << " is larger\n";

    foo();

    return 0;
}

image

posted @ 2025-12-24 17:48  游翔  阅读(20)  评论(0)    收藏  举报