Skip to content

Latest commit

 

History

History
222 lines (190 loc) · 12.6 KB

File metadata and controls

222 lines (190 loc) · 12.6 KB

Table of Contents generated with DocToc

第二章:类模板

定义类模板

template<typename T>
class ClassName
{
    ...
};
  • 同样,惯例使用T作为模板类型参数名。但当模板类型参数多了以后,使用T1,T2,...就不是那么好了。可以根据含义来命名。
  • typename同样可以使用class代替。但通常我们使用typename
  • 在类模板内部,T可以被像其他类型一样用来定义数据成员或者成员函数。
  • 使用类模板是必须跟上模板实参,除了C++17引入的类模板实参推导。
  • 在类模板定义中使用类模板名称不带模板实参代表其使用其定义时的模板参数。在类模板中指代自己通常都是这样用。
  • 在类模板外部则需要显式指定模板参数。我们都知道在类外定义时在类名::后才进入类的作用域,在此之前都必须写模板参数。
  • 在需要名称而不是类型的地方也不能加模板参数,典型就是构造函数和析构函数名称。

类模板的成员函数实现:

  • 在例外是实现成员函数时必须指出这是一个类模板的成员函数:
template<typename T>
void ClassName<T>::doSomething()
{
    ...
}
  • 内部实现则会简单许多,就像普通类成员函数类内实现那样。

使用类模板

  • 在C++17之前,使用类模板时,我们都必须指定类模板实参。
  • C++17引入了类模板实参推导:如果模板实参能够仅有类模板构造函数推导出来,那么可以不用显式指定。
  • 在类模板实例化时,仅会实例化其使用到的成员函数,未使用到的则不会实例化。这种情况下,可以节省空间并且部分地使用类模板。
  • 实例化后的类模板类型和普通类型一样,可以使用CV修饰或者从其派生出数组、指针和引用等复合类型,也可以将其用来定义类型别名,或者作为其他模板的模板类型实参。
  • C++11之前必须在两个模板的后角括号间加入一个空格,如stack<stack<int> >,C++11之后则不再需要。

部分使用类模板

  • 类模板参数需要满足一定的条件(也称为有效表达式约束),也就是类模板中用到了的类模板参数的操作需要是合法的,否则程序就是非良构的(要么编译通不过,要么运行时会出问题)。
  • 但是因为类模板实例化时只会实例化用到的成员函数,所以事实上只需要用到的操作能够得到满足即可。
  • 所以某些时候甚至可以在定义一个类模板设定多个层次的要求,满足最低限度要求即可使用部分操作,这提供了相当大程度的灵活性。

概念:

  • 概念(concept)这个名词用来表示模板参数所需要支持的一系列约束。
  • 当然直到C++17,concept都是通过注释中的信息之类的方式来表述的,而没有成为一个语法。这就导致了模板的报错(会包含整个实例化的历史:从实例化的地方一直到模板的定义)通常冗长且难以理解。
  • 直到C++20 概念库的引入,concept才被标准化了。因为这本书是基于C++17的,没有涉及到太多concept的东西,所以其语法会在后面抽空额外探究一下。
  • 在concept之前,也有一些手段来做这件事情,比如static_assert,可以一定程度上增强报错信息的可读性。
  • 附录E讨论了Concepts,相关细节在这里讨论。

友元

  • 如果在一个友元函数中中仅仅使用了类模板中定义的模板类型参数,而未引入新的模板参数。那么这个友元函数则是一个普通函数(但是它使用了类模板的模板参数,所以它必须是一个函数模板的特化)。
  • 而如果引入的新的模板参数,不依赖于类模板的类型参数,那么这个友元函数将是一个独立的函数模板。
  • 例子:
template<typename T> class stack;
template<typename T>
std::ostream& operator<< (std::ostream& os, const Stack<T>& s);
template<typename T>
class Stack
{
    // option 1
    friend std::ostream& operator<< <T>(std::ostream& os, const Stack<T>& s)
    {
        ...
    }
    // option 2
    template<typename U>
    friend std::ostream& operator<<(std::ostream& os, const Stack<U>& s)
    {
        ...
    }
};
  • 这里引入了一个问题:如何声明这个友元函数的问题。
  • 上面的选择1和选择2拥有不同的行为。
  • 选择1:在友元函数中使用类模板参数。那么它就是必须是一个函数模板对类模板参数的特化,所以这个函数模板需要一个前向声明,而前向声明中又用到了类模板,所以类模板也需要一个前向声明。
  • 选择2:就是简单将函数模板定义为友元。
  • 选择1和2的含义是不同的,选择1仅是类模板参数对应的模板函数特化成为友元。选择2则是该类模板所有特化都成为友元。
  • 通常来说友元函数中我们只会使用对应模板参数的类型。这使得两者都是可用且可行的。选择1会麻烦一些,但是表达的含义会更加准确。选择2更简单,但是引入了额外不需要的含义,但这通常不会造成任何程序逻辑问题。

类模板特化

  • 类模板同样可以针对特定类型进行特化。
  • 特化的目的通常是针对一些类型进行优化或者修复某些类型的不良行为。比如vector<bool>(尽管大部分人会觉得这是一个典型的失败设计)。
  • 当特化一个类模板时,必须特化其所有的成员函数。
  • 当然也可以特化其中一个成员函数,这时不必特化整个类模板。
  • 类模板(全)特化的格式:
template<>
class ClassName<xxx>
{
    ...
};
  • 对于全特化而言,模板参数已经固定,所以成员函数都必须定义为普通函数。其中每个模板类型参数都必须替换为特化的类型实参。
  • 当然特化时修改定义是完全可行,这里说要特化其所有的成员函数并且定义要匹配是为了使得该特化能够兼容类模板定义,让我们感知不到这是一个特化。将其像类模板那样使用,这是一个编程实践方面的原则,而非语法规定。
  • 特化通常来说要保持接口一致,内部实现则可以千差万别。

类模板偏特化

  • 上面的特化指的是全特化,而类模板也可以被部分特化(partial sepcialization,通常称为偏特化)函数模板则不可以偏特化。
  • 可以使用类模板偏特化提供特定场景下的特殊实现。而其中某些模板参数依然要由用户来实现。也就是说偏特化还是一个类模板,还是有模板参数。
  • 比如我们可以为指针类型提供一个偏特化:依然提供模板参数T,但是为其指针类型T*特化。
template<typename T>
class ClassName<T*>
{
    ...
};

多个模板参数的特化:

  • 我们也可以为多个模板的类模板进行偏特化,可以有各种手段:
    • 固定某几个模板参数为特定类型,剩余的依然保持为模板参数。
    • 为其中某些参数提供复合类型特化(指针、数组、引用)。
    • 将两个模板参数偏特化为同一个模板参数,使用一个模板参数代替。
  • 偏特化将比类模板定义版本更加优先,但如果某个实例化匹配了多个偏特化版本。那么将会有二义性,造成编译报错。
  • 有一系列手段解决偏特化二义性的问题,有很多细节,后续会详述(第16章)。

默认类模板参数

  • 类似于函数模板,同样可以给类模板提供默认实参。
  • 类似于函数参数,类模板默认实参必须从后往前添加。(但是函数模板的默认模板参数则不必要从后往前添加,因为函数模板可以进行模板参数类型推导)。

类型别名

为一个普通类型定义别名:

  • C++11前,经典定义方式:typedef Stack<int> IntStack;
  • C++11后:使用using进行类型别名声明(type alias declaration),using IntStack = Stack<int>;
  • 这个新名称叫做类型别名(type alias)。
  • C++11后,我们更倾向于使用using进行别名声明。

别名模板(alias template):

  • 使用using可以对一个模板进行别名声明,typedef则不行。
  • 用法:
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;
  • 别名和原始名称都代表同一个类型,没有任何区别。
  • 需要注意的是,通常来说,模板仅能够声明或者定义在全局/命名空间作用域或者类声明中。
  • 别名模板对于某些嵌套类型的别名定义尤其有用。

类型特性中的_t后缀:

  • 在C++14之后,标准库中处理类型萃取的类型特性通常会提供一个_t的别名模板。如std::add_const_t<T>就是std::add_const<T>::type
  • 编码时在类型别名后加一个_t后缀是一个好的命名风格。
  • 同理值萃取通常会使用_v后缀提供模板变量别名。

类模板实参推导

  • 在C++17之前,类模板实例化时,模板参数任何情况下都需要传入,除非有默认实参。
  • C++17开始,这个要求被放宽了,即是没有默认实参,只要构造函数能够推导出模板参数,也能够省略定义时的模板实参。
  • 例子:
std::vector<int> vec;
std::vector vec2 = vec;
  • 不同于函数模板可以部分推导模板参数,只传递剩余的模板参数。类模板只能推导全部参数,只要有一个不能被推导出,就必须传入所有模板参数。

字符串字面值推导:

  • 字符串字面值推导时会根据构造函数的参数是值传递还是引用传递推导出不同类型,值传递会退化为指针,而引用传递则不会。
  • 数组同理。

推导指引(deduction guides):

  • C++17同类模板实参推导一起引入的还有一个推导指引语法(deduction guides),即是可以允许用户自定义推导时该推导为什么类型。
  • 语法:
Stack(char const*) -> Stack<std::string>;
  • 前面是构造函数签名,后面是推导出的类模板实例化类型,用->连接,并且应该放在和类定义一个作用域下,通常紧跟在类定义后。
  • 推导指引也可以是模板,比如标准库中std::vector的推导指引:
template< class InputIt,
          class Alloc = std::allocator<typename std::iterator_traits<InputIt>::value_type>>
vector(InputIt, InputIt, Alloc = Alloc())
  -> vector<typename std::iterator_traits<InputIt>::value_type, Alloc>;

模板化聚合类

聚合类:

  • 是指满足下列条件的类:没有用户提供的显式定义的或者继承的构造函数,没有私有的或者保护的非静态数据成员,没有虚函数,没有虚基类、私有或保护继承来的基类。
  • 聚合类可以是模板类。
  • C++17以后甚至可以为模板聚合类定义推导指引,如果没有推导指引,类模板实参推导则是不可能的,因为聚合类压根就没有自定义的构造函数。
  • 标准库的std::array<>同样是一个聚类类模板。
  • 其推导指引:
template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;

总结

  • 类模板中,仅有使用到的成员函数会被实例化。
  • 可以为特定类型特化类模板。
  • 是可以部分特化(偏特化)类模板。
  • C++17之后,类模板参数可以通过构造函数推导。
  • 可以定义聚合类模板。
  • 参数值传递时,模板类型推导出来是退化后的类型。
  • 模板仅能在全局作用域、命名空间作用域和类声明中定义。