在《函数模板》中,我们介绍了函数模板:
template <typename T> // 模板形参声明
T max(T x, T y) // 函数模板定义
{
return (x < y) ? y : x;
}
函数模板允许我们声明类型形参(如 typename T
),并将其用作函数参数的类型(T x, T y
)。
在第 13.13 课《类模板》中,我们又学习了类模板,它允许把类型形参用于数据成员的类型:
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
// C++17 及更早版本需手动提供推导指引
template <typename T>
Pair(T, T) -> Pair<T>;
int main()
{
Pair<int> p1{ 5, 6 }; // 实例化 Pair<int>
Pair<double> p2{ 1.2, 3.4 }; // 实例化 Pair<double>
Pair<double> p3{ 7.8, 9.0 }; // 复用已生成的 Pair<double>
}
相关说明见《类模板实参推导(CTAD)与推导指引》。
本节将结合函数模板与类模板,深入探讨带成员函数的类模板。
把类型形参用于成员函数
在类模板形参声明中定义的类型形参,既可用作数据成员的类型,也可用作成员函数的形参类型。
下面把之前的 Pair
结构体重写为类,并添加成员函数:
#include <ios> // std::boolalpha
#include <iostream>
template <typename T>
class Pair
{
private:
T m_first{};
T m_second{};
public:
// 类体内定义成员函数时,可直接使用类的模板形参
Pair(const T& first, const T& second)
: m_first{ first }, m_second{ second }
{
}
bool isEqual(const Pair<T>& pair);
};
// 类体外定义成员函数时,需重新提供模板形参声明
template <typename T>
bool Pair<T>::isEqual(const Pair<T>& pair)
{
return m_first == pair.m_first && m_second == pair.m_second;
}
int main()
{
Pair p1{ 5, 6 }; // CTAD 推导出 Pair<int>
std::cout << std::boolalpha
<< "isEqual(5, 6): " << p1.isEqual(Pair{5, 6}) << '\n'
<< "isEqual(5, 7): " << p1.isEqual(Pair{5, 7}) << '\n';
}
几点说明:
- 由于存在私有成员,该类不再是聚合类,需使用构造函数初始化。
- 构造函数形参使用
const T&
,避免潜在的高成本拷贝。 - 类体内定义的成员函数,隐式使用类模板的模板形参,无需额外声明。
- 非聚合类通过匹配构造函数即可启用 CTAD,无需手写推导指引。
- 类体外定义成员函数时:
- 必须重新给出
template <typename T>
前缀; - 成员函数名需用完整模板类名限定:
Pair<T>::isEqual
。
- 必须重新给出
注入类名(Injected Class Name)
构造函数名须与类名完全一致。然而上述模板中,我们写的是 Pair
而非 Pair<T>
,却依旧合法。
在类作用域内,未经限定的类名称为注入类名。对于类模板,注入类名是完整模板名的简写。
在 Pair<T>
作用域中,Pair
即 Pair<T>
,因此构造函数名与类名匹配。同理,可把 isEqual
的形参写成:
template <typename T>
bool Pair<T>::isEqual(const Pair& pair) // Pair 即 Pair<T>
{
return m_first == pair.m_first && m_second == pair.m_second;
}
关键洞见
CTAD 不适用于函数形参(仅用于实参推导)。但在类模板作用域内使用注入类名作为形参类型是合法的,因为它并非 CTAD,而是完整模板名的简写。
类模板成员函数的外部定义位置
编译器需同时看到:
- 类定义(确保成员函数模板声明存在);
- 成员函数模板定义(用于实例化)。
因此,类模板及其成员函数模板通常放在同一文件。
- 若成员函数模板定义在类体内,则随类定义一同可见,简单但会增大类体。
- 若定义在类体外,应紧随类定义之后放置,确保可见性一致。
当类定义位于头文件时,这些外部定义也应放在同一头文件中。
关键洞见
从模板隐式实例化得到的函数均为隐式内联。因此,将成员函数模板置于头文件并被多个源文件包含不会引发多重定义,链接器会自动去重。
最佳实践
凡在类体外定义的成员函数模板,应紧邻类定义之后、置于同一文件内。
小测验
问题 1
编写一个名为 Triad
的类模板,含 3 个私有数据成员,各自使用独立的类型形参。要求提供构造函数、访问函数及在类外定义的 print()
成员函数。以下程序应能编译运行:
#include <iostream>
#include <string>
int main()
{
Triad<int, int, int> t1{ 1, 2, 3 };
t1.print();
std::cout << '\n';
std::cout << t1.first() << '\n';
using namespace std::literals::string_literals;
const Triad t2{ 1, 2.3, "Hello"s };
t2.print();
std::cout << '\n';
return 0;
}
并输出:
[1, 2, 3]
1
[1, 2.3, Hello]
问题 2
若将 print()
的声明与定义中的 const
去掉,程序将无法编译。原因是什么?