15-5 带成员函数的类模板

在第11.6节——函数模板中,我们探讨了函数模板:

template <typename T> // this is the template parameter declaration
T max(T x, T y) // this is the function template definition for max<T>
{
    return (x < y) ? y : x;
}

使用函数模板时,我们可以定义类型模板参数(例如 typename T),然后将其用作函数参数的类型(T x, T y)。

第 13.13 节——类模板中,我们介绍了类模板,它允许我们为类类型(结构体、类和联合体)的数据成员类型使用类型模板参数:

#include <iostream>

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

// Here's a deduction guide for our Pair (required in C++17 or older)
// Pair objects initialized with arguments of type T and T should deduce to Pair<T>
template <typename T>
Pair(T, T) -> Pair<T>;

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

相关内容:
我们在第13.14节讨论推导指南——类模板参数推导(CTAD)与推导指南

在本节课中,我们将结合函数模板和类模板的元素,深入探讨具有成员函数的类模板。


在成员函数中使用类型模板参数

作为类模板参数声明一部分定义的类型模板参数,既可用于数据成员的类型,也可用于成员函数参数的类型。

在下面的示例中,我们将重写上述Pair类模板,将其从结构体转换为类:

#include <ios>       // for std::boolalpha
#include <iostream>

template <typename T>
class Pair
{
private:
    T m_first{};
    T m_second{};

public:
    // When we define a member function inside the class definition,
    // the template parameter declaration belonging to the class applies
    Pair(const T& first, const T& second)
        : m_first{ first }
        , m_second{ second }
    {
    }

    bool isEqual(const Pair<T>& pair);
};

// When we define a member function outside the class definition,
// we need to resupply a template parameter declaration
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 }; // uses CTAD to infer type Pair<int>
    std::cout << std::boolalpha << "isEqual(5, 6): " << p1.isEqual( Pair{5, 6} ) << '\n';
    std::cout << std::boolalpha << "isEqual(5, 7): " << p1.isEqual( Pair{5, 7} ) << '\n';

    return 0;
}

以上内容应该相当直观,但有几点值得注意。

首先,由于我们的类包含私有成员,它不属于聚合类型,因此无法使用聚合初始化。我们必须通过构造函数来初始化类对象。

由于类数据成员类型为 T,构造函数参数定义为 const T&,以便用户提供同类型初始化值。鉴于 T 可能存在高开销的复制操作,采用常量引用传递比值传递更安全。

需注意:在类模板定义内部声明成员函数时,无需额外声明模板参数。此类成员函数将隐式继承类模板的参数声明。

其次,对于非聚合类,CTAD(构造函数推导)无需额外推导指引。匹配的构造函数本身就为编译器提供了从初始化项推导模板参数所需的信息。

第三,让我们更深入地分析在类模板定义外部定义成员函数的情况:

template <typename T>
bool Pair<T>::isEqual(const Pair<T>& pair)
{
    return m_first == pair.m_first && m_second == pair.m_second;
}

由于该成员函数定义与类模板定义分离,我们需要重新提供模板参数声明(template ),以便编译器识别T的类型。

此外,当在类外部定义成员函数时,需使用类模板的完整模板名称修饰成员函数名(即Pair::isEqual而非Pair::isEqual)。


注入类名

在之前的课程中,我们提到构造函数的名称必须与类名一致。但在上文的Pair类模板中,我们将其构造函数命名为Pair而非Pair。尽管名称不匹配,这段代码却依然能正常运行。

在类作用域内,未限定的类名被称为注入类名injected class name。在类模板中,注入类名可作为完整模板名的简写形式。

由于Pair是Pair的注入类名,在Pair类模板作用域内,任何对Pair的调用都会被视为Pair。因此,尽管构造函数命名为Pair,编译器仍将其视为Pair——此时名称已匹配!

这意味着我们也可以这样定义isEqual()成员函数:

template <typename T>
bool Pair<T>::isEqual(const Pair& pair) // note the parameter has type Pair, not Pair<T>
{
    return m_first == pair.m_first && m_second == pair.m_second;
}

由于这是Pair成员函数的定义,我们处于Pair类模板的作用域内。因此,任何对Pair的使用都是对Pair的简写形式!

关键要点

第13.14节——类模板参数推导(CTAD)与推导引导中,我们提到CTAD不适用于函数参数(因其是参数argument推导而非参数parameter推导)。但将注入的类名作为函数参数是可行的,因为这属于完整模板名的简写形式,而非CTAD的应用场景。


在类外部定义类模板成员函数的位置

对于类模板的成员函数,编译器需要同时看到类定义(以确保成员函数模板作为类的一部分被声明)和模板成员函数定义(以了解如何实例化该模板)。因此,我们通常希望在同一位置定义类及其成员函数模板。

当成员函数模板定义在类定义内部时,该模板成员函数定义即属于类定义的一部分,因此在类定义可见的任何位置,模板成员函数定义同样可见。这种方式操作简便(代价是使类定义显得杂乱)。

当成员函数模板定义在类定义之外时,通常应紧接在类定义下方定义。这样,凡能看到类定义之处,也能看到紧接在类定义下方的成员函数模板定义。

在类通常定义于头文件的典型情况下,这意味着任何定义在类外部的成员函数模板也应定义在同一头文件中,且位于类定义下方。

关键要点:
在第11.7节——函数模板实例化中,我们提到从模板隐式实例化的函数会隐式内联。这包括非成员函数模板和成员函数模板。因此,将定义在头文件中的成员函数模板包含到多个代码文件中不会有问题,因为从这些模板实例化的函数会隐式内联(链接器会消除它们的重复)。

最佳实践:
任何定义在类定义外部的成员函数模板,都应紧接在类定义下方(同一文件内)进行定义。


测验时间

问题 #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]

image

显示解答

#include <iostream>
#include <string>

template <typename T, typename U, typename V>
class Triad
{
private:
	T m_first{};
	U m_second{};
	V m_third{};

public:
	Triad(const T& first, const U& second, const V& third)
		: m_first{ first }
		, m_second{ second }
		, m_third{ third }
	{
	}

	const T& first() const { return m_first; }
	const U& second() const { return m_second; }
	const V& third() const { return m_third; }

	void print() const;
};

template <typename T, typename U, typename V>
void Triad<T, U, V>::print() const
{
	std::cout << '[' << m_first << ", " << m_second << ", " << m_third << ']' ;
}

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

问题 #2

如果我们从 print() 函数的声明和定义中移除 const 关键字,程序将无法编译。为什么?

显示解答

t2 是 const 对象,因此只能在其上调用 const 成员函数。若将 print() 改为非 const 成员函数,则 t2 将无法调用它。这是因为非 const 成员函数可能修改隐含对象,从而违反 const 对象(此处为 t2)的 const 属性。
posted @ 2026-01-02 17:15  游翔  阅读(10)  评论(0)    收藏  举报