CTTCG 02 Class Templates

2023 年 5 月 7 日

定义位置

类模板只能在全局或者类声明中定义,不能在块或者函数中定义。

类模板外函数定义的写法

类模板中定义函数,T 类型参数可以省略;在类模板中声明,但在外提供函数定义,就需要在定义处给出完整的类型参数:

template<typename T> void Stack<T /* 不能省略 */>::push (T const& elem) { /* impl */ }

一个例外是析构函数和构造函数的名称处,因为这里不表类型,而是表示函数名。

template<typename T> void Stack<T>::Stack() { /* impl */ }
//                                   ^^^^ 这里不能加参数T

P30 处理友元函数,比如向输出流打印。最简单的还是直接在类模板中提供定义:

friend std::ostream& operator<<(std::ostream& os, Stack const& s) {
    for (auto const& i : s.elems) os << i << ' ';
    return os;
}

如果只在类模板中写声明,要么 ① 把函数写成模板函数,并使用不同的类型参数符号,然后在类外补上定义:

template <class U> friend std::ostream& operator<<(std::ostream& os, Stack<U> const& s);

② 要么先声明类模板,再声明这个模板函数,然后在类中写:

friend std::ostream& operator<< <>(std::ostream& os, Stack const& s);

其中<>表示函数需要匹配某个模板。最后在类外补上定义。

按需实例化

类模板只会实例化那些被用到的成员函数。这虽然降低了空间开销,但也使得一些问题很难被发现,比如常常在添加新的功能(调用之前没被调用的函数)后才发现旧代码无法编译的问题。

特化

可以用 template <> 特化类模板。若特化类模板要在外给出函数定义,只需要把他当成普通的类,不要再加 template <>

template<> class Stack<std::string> { /* impl */ };
void Stack<std::string>::push (std::string const& elem) /* impl */

偏特化

template<typename T>
class Stack<T*> { /* impl */ }
//         ^^^ 注意这个尖括号是相对类模板的定义多出来的

偏特化任何模板时不能再提供默认模板参数,省略会导致继承默认参数,不省略则可以额外指定。特化函数模板时不能再提供函数默认参数值,但也不能省略参数声明。(一个是写在尖括号 <> 里的,一个是写在小括号 () 里面的。)

偏特化是可以有更多的模板参数的,这样才能表达更加复杂和具体的结构,比如任意一个指向成员的指针至少需要两个模板参数来精确表达:

template<typename T, typename C>
class List<T* C::*> {
    // 略
};

默认参数

类模板的默认参数应该放到靠后的位置,因为类模板是不能通过参数推导的。这个和模板函数注意对比。

Alias Templates

namespace std {
    template<typename T> using add_const_t = typename add_const<T>::type;
}

alias templates 不能被特化。

C++ 17 省略类模板参数

Stack<int> intStack1;
Stack<int> intStack2 = intStack1;
Stack intStack3 = intStack1; // 能推断出类型参数是 int

由于推导的类型不一定是我们想要的,我们有时应该尽量在成员函数中写成接受值(而不是引用)的形式,从而使得数组能退化成首元素指针。P41

存在疑问时,类模板参数推导倾向于拷贝,而不是将参数作为子元素。P43

C++ 类模板在推导参数时不支持偏特化。就算是空参数也不行(空参数是 Java 的语法)。比如 Stack stk {"2"}; 正确,而 Stack<> stk {"2"}; 错误。

Deduction Guide

Stack(char const*) -> Stackstd::string;

对于聚合类模板也可以定义推导规则,比如:

template<typename T>
struct ValueWithComment {
    T value;
    std::string comment;
};

ValueWithComment(char const*, char const*)
  -> ValueWithComment<std::string>;

这样 ValueWithComment vc2 = {"hello", "initial value"}; 就会被推导规则引导。如果没有这条规则,代码就无法通过编译,因为推导是根据类模板构造函数的参数进行的,而聚合类没有构造函数。

注意,C++20 不需要 deduction guide 也可以编译上面的代码,应该是聚合类的定义、类模板参数推导要求有改动。

其他:

  • 还能对推导规则添加 explicit 关键字。
  • deduction guide 本身可以是模板:template<typename T> S(T) -> S<T>;
  • 和 auto 一样,当模板类参数被省略时,想要在同一语句中声明两个变量,必须保证他们的参数类型一致。
  • deduction guide 本身只用于引导参数推导,不用于实际调用。因此对于多个引用类型(左引、右引)的构造函数,只需要写一个“值类型作为参数”的推导规则。