模板类初步
以下以Stack
类举例。每当在声明中使用此类的类型时,都必须使用Stack<T>
,除非模板参数可以推断。但是,在类模板内部,使用类名(后面不跟模板参数)表示以模板参数作为参数的类。
1 |
|
注意以上实现并不确保异常安全。
代码仅对调用的模板(成员)函数进行实例化。对于类模板,成员函数仅在使用时才进行实例化。这当然可以节省时间和空间,并允许仅部分使用类模板。
类模板通常会对其实例化的模板参数应用多项操作(包括构造和析构)。这可能会让人觉得这些模板参数必须提供类模板所有成员函数所需的所有操作。但事实并非如此:模板参数只需提供所需的所有必要操作(而不是可能需要的操作)。我们如何知道模板需要哪些操作才能实例化?术语 concept (C++20支持) 通常用于表示模板库中重复需要的一组约束。例如,C++标准库依赖于随机访问迭代器和默认构造等概念。
友元
对于Stack
类,operator<<
必须作为非成员函数实现。
1 | template<typename T> class Stack { |
请注意,上述实现意味着类Stack<>
的operator<<
不是一个函数模板,而是一个在需要时用类模板实例化的 普通函数 ( templated entity )。
但是,当尝试声明友元函数并在之后才定义它时,事情会变得更加复杂。我们有如下两个选择:
第一种选择是:我们可以隐式声明一个新的函数模板,它必须使用不同的模板参数,例如U
:
1 | template<typename T> class Stack { |
再次使用T
或跳过模板参数声明都是错误的(要么内部T
隐藏外部T
,要么我们在命名空间范围内声明一个非模板函数)。
第二种选择是:我们可以将Stack<T>
的输出运算符前向声明为模板,但这意味着我们必须首先前向声明Stack<T>
:
1 | template<typename T> class Stack; |
之后,我们可以以友元声明该函数:
1 | template<typename T> class Stack { |
请注意"函数名"operator<<
后面的<T>
。因此,我们将非成员函数模板的特化声明为友元。如果没有<T>
,我们将声明一个新的非模板函数!
类模板的特化 (Specialization)
可以针对某些模板参数对类模板进行特化。与函数模板的重载类似,对类模板进行特化允许针对某些类型优化实现,或针对类模板的实例化修复某些类型的错误行为。但是,如果特化了一个类模板,还必须特化所有成员函数。
要特化类模板,必须使用前导模板<>
和类模板特化类型的规范来声明类。类型用作模板参数,必须在类名称后直接指定。
1 |
|
在这个例子中,specialization使用引用语义将字符串参数传递给push()
,这对于这个特定类型来说更有意义(尽管我们最好传递一个forwarding reference)。另一个区别是使用deque
而不是vector
来管理Stack
内的元素。虽然这在这里没有什么特别的好处,但它确实表明特化的实现可能看起来与主模板的实现非常不同。
部分特化 (Partial Specialization)
类模板可以部分特化。可以为特定情况提供特殊实现,但某些模板参数仍必须由用户定义。
1 |
|
使用方法如下:
1 | Stack<int *> ptrStack; |
默认类模板参数 (Default Class Template Arguments)
对于函数模板,可以为类模板参数定义默认值。例如,在类Stack<>
中,可以将用于管理元素的容器定义为第二个模板参数,并使用std::vector<>
作为默认值:
1 |
|
使用方法如下:
1 | template <typename T> using DequeStack = Stack<T, std::deque<T>>; |
类型别名 (Type Aliases)
要简单地为完整类型定义一个新名称,有两种方法可以做到:
- Typedef name:使用
typedef
; - Alias declaration (建议使用):使用
using
。
与typedef
不同,alas declaration可以模板化,以便为类型系列提供方便的名称。这在C++11之后可用,称为 alias template。
自C++14起,标准库使用Type Traits Suffix _t
技术为标准库中产生类型的所有type traits定义快捷方式。例如,可以编写:
1 | std::add_const_t<T> // 自 C++14 起 |
而不是
1 | typename std::add_const<T>::type // 自 C++11 起 |
标准库定义:
1 | namespace std { |
类模板参数推导 (Class Template Argument Deduction)
在C++17之前,总是必须将所有模板参数类型传递给类模板(除非它们具有默认值)。从C++17开始,总是必须显式指定模板参数的约束被放宽了。相反,如果构造函数能够推断出所有模板参数(没有默认值),则可以跳过显式定义模板参数。
通过提供传递一些初始参数的构造函数,可以支持推断Stack
的元素类型。例如,我们可以提供一个可由单个元素初始化的Stack
:
1 | template<typename T> |
这使得可以按如下方式声明一个Stack
:
1 | Stack intStack = 0; // Stack<int> |
通过用整数0
初始化堆栈,模板参数T
被推导为int
,从而实例化Stack<int>
。
请注意由于int
构造函数的定义,必须请求默认构造函数以其默认行为可用,因为只有在未定义其他构造函数时,默认构造函数才可用。
注意,与函数模板不同,类模板参数不能仅部分推断 (通过仅明确指定 部分 模板参数)。
可以定义特定的 deduction guides 来提供额外的或修复现有的类模板参数推导。例如,可以定义每当传递字符串文字或C字符串时,都会为std::string
实例化Stack
:
1 | Stack(char const*) -> Stack<std::string>; |
此引导必须出现在与类定义相同的范围(命名空间)中。通常,它在类定义之后。我们将遵循->
的类型称为deduction guide的 guided type。
1 | Stack stringStack{"bottom"}; // OK: Stack<std::string> deduced since C++17 |
模板化聚合 (Templatized Aggregates)
聚合类 (Aggregate classes) (没有用户提供的、显式的或继承的构造函数、没有私有的或受保护的非静态数据成员、没有虚函数、也没有虚的、私有的或受保护的基类的类/结构体) 也可以是模板。例如:
1 | template<typename T> |
定义一个聚合类,该聚合类针对其所持有的值value
的类型进行参数化。可以像声明任何其他类模板一样声明对象,并且仍将其用作聚合类:
1 | ValueWithComment<int> vc; |
从C++17开始,可以为聚合类模板定义deduction guide:
1 | ValueWithComment(char const*, char const*) -> ValueWithComment<std::string>; |
如果没有deduction guide,初始化就不可能实现,因为ValueWithComment
没有构造函数来执行推导。