C++ 带成员函数的类模板:深入理解与实践

在《函数模板》中,我们介绍了函数模板:

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';
}

几点说明:

  1. 由于存在私有成员,该类不再是聚合类,需使用构造函数初始化。
  2. 构造函数形参使用 const T&,避免潜在的高成本拷贝。
  3. 类体内定义的成员函数,隐式使用类模板的模板形参,无需额外声明。
  4. 非聚合类通过匹配构造函数即可启用 CTAD,无需手写推导指引。
  5. 类体外定义成员函数时:
    • 必须重新给出 template <typename T> 前缀;
    • 成员函数名需用完整模板类名限定:Pair<T>::isEqual

注入类名(Injected Class Name)

构造函数名须与类名完全一致。然而上述模板中,我们写的是 Pair 而非 Pair<T>,却依旧合法。

在类作用域内,未经限定的类名称为注入类名。对于类模板,注入类名是完整模板名的简写。

Pair<T> 作用域中,PairPair<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 去掉,程序将无法编译。原因是什么?

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"