类模板
在函数模板中,我们介绍了为每种不同类型的组合创建单独的(重载的)函数所面临的挑战:
#include <iostream>
// 计算两个int值中较大的函数
int max(int x, int y)
{
return (x < y) ? y : x;
}
// 几乎相同的函数,用于计算两个double值中较大的值
// 唯一的区别是类型信息
double max(double x, double y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(5, 6); // 调用 max(int, int)
std::cout << '\n';
std::cout << max(1.2, 3.4); // 调用 max(double, double)
return 0;
}
解决方法是创建一个函数模板,编译器可以使用它来为我们需要的任何类型组合实例化普通函数:
#include <iostream>
// max的单一函数模板
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(5, 6); // 实例化并调用 max<int>(int, int)
std::cout << '\n';
std::cout << max(1.2, 3.4); // 实例化并调用 max<double>(double, double)
return 0;
}
相关内容
我们在第11.7课——函数模板实例化中介绍了函数模板实例化的工作原理。
聚合类型面临的类似挑战
我们在聚合类型(包括结构体/类/联合体和数组)中也遇到了类似的挑战。
例如,假设我们正在编写一个程序,需要处理整数对,并需要确定这两个数字中哪个更大。我们可能会编写如下程序:
#include <iostream>
struct Pair
{
int first{};
int second{};
};
constexpr int max(Pair p) // 按值传递,因为Pair很小
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair p1{ 5, 6 };
std::cout << max(p1) << " is larger\n";
return 0;
}
后来,我们发现还需要处理双精度浮点数对。于是我们更新了程序如下:
#include <iostream>
struct Pair
{
int first{};
int second{};
};
struct Pair // 编译错误:错误地重新定义了Pair
{
double first{};
double second{};
};
constexpr int max(Pair p)
{
return (p.first < p.second ? p.second : p.first);
}
constexpr double max(Pair p) // 编译错误:重载函数仅通过返回类型不同
{
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;
}
不幸的是,这个程序无法编译,并且存在许多需要解决的问题。
首先,与函数不同,类型定义不能重载。编译器会将Pair
的第二次定义视为对第一次定义的错误重声明。其次,尽管函数可以重载,但我们的max(Pair)
函数仅通过返回类型不同,而重载函数不能仅通过返回类型来区分。第三,这里存在大量冗余。每个Pair
结构体除了数据类型外都是相同的,我们的max(Pair)
函数也是如此(除了返回类型)。
我们可以通过为Pair
结构体使用不同的名称(例如PairInt
和PairDouble
)来解决前两个问题。但这样我们不仅需要记住我们的命名方案,而且还需要为每种额外的对类型克隆大量代码,这并没有解决冗余问题。
幸运的是,我们可以做得更好。
作者注
在继续之前,请复习第11.6课——函数模板和第11.7课——函数模板实例化,如果你对函数模板、模板类型或函数模板实例化的工作原理不太熟悉的话。
类模板
就像函数模板是实例化函数的模板定义一样,类模板是实例化类类型的模板定义。
提醒一下,“类类型”是指结构体、类或联合体类型。尽管我们为了简单起见将在结构体上演示“类模板”,但这里的内容同样适用于类。
提醒一下,这是我们的int
对结构体定义:
struct Pair
{
int first{};
int second{};
};
让我们将我们的对类重写为类模板:
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // 实例化 Pair<int> 并创建对象 p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // 实例化 Pair<double> 并创建对象 p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // 使用之前的 Pair<double> 定义创建对象 p3
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}
就像函数模板一样,我们以模板参数声明开始类模板定义。我们首先使用template
关键字。接下来,我们在尖括号(<>
)内指定类模板将使用的所有模板类型。对于我们需要的每个模板类型,我们使用关键字typename
(推荐)或class
(不推荐),后跟模板类型的名称(例如T
)。在这个例子中,由于我们的两个成员将是相同的类型,我们只需要一个模板类型。
接下来,我们像往常一样定义我们的结构体,只是我们可以在任何需要稍后将被实际类型替换的模板类型的地方使用我们的模板类型(T
)。就是这样!我们完成了类模板定义。
在main
函数中,我们可以使用任何我们想要的类型来实例化Pair
对象。首先,我们实例化了一个Pair<int>
类型的对象。因为Pair<int>
的类型定义还不存在,编译器使用类模板来实例化一个名为Pair<int>
的结构体类型定义,其中所有模板类型T
的出现都被int
类型替换。
接下来,我们实例化了一个Pair<double>
类型的对象,这将实例化一个名为Pair<double>
的结构体类型定义,其中T
被double
替换。对于p3
,Pair<double>
已经实例化,因此编译器将使用之前的类型定义。
以下是与上面相同的示例,展示了编译器在所有模板实例化完成后实际编译的内容:
#include <iostream>
// 我们的Pair类模板的声明
// (我们不再需要定义,因为它不再被使用)
template <typename T>
struct Pair;
// 显式定义Pair<int>的样子
template <> // 告诉编译器这是一个没有模板参数的模板类型
struct Pair<int>
{
int first{};
int second{};
};
// 显式定义Pair<double>的样子
template <> // 告诉编译器这是一个没有模板参数的模板类型
struct Pair<double>
{
double first{};
double second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // 实例化 Pair<int> 并创建对象 p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // 实例化 Pair<double> 并创建对象 p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // 使用之前的 Pair<double> 定义创建对象 p3
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}
你可以直接编译这个示例,并看到它按预期工作!
对于高级读者
上述示例使用了一个名为类模板特化的功能(将在未来的第26.4课——类模板特化中介绍)。目前不需要了解这个功能的工作原理。
在函数中使用我们的类模板
现在让我们回到让我们的max()
函数能够处理不同类型数据的挑战。因为编译器将Pair<int>
和Pair<double>
视为不同的类型,我们可以使用通过参数类型区分的重载函数:
constexpr int max(Pair<int> p)
{
return (p.first < p.second ? p.second : p.first);
}
constexpr double max(Pair<double> p) // 好的:通过参数类型区分的重