这是C++语言的一系列文章,内容取自于网易微专业《C++开发工程师(升级版)》。

本文的主题是 模板基本概念与STL容器简介。

Part 1:模板简介

模板允许参数化的类型,它可以根据类型参数生成函数和类。它是对类或函数更高层次的抽象。它可以分为两类:类模板和函数模板。 类模板根据传入的类型参数定义了一组关联的类;函数模板则定义了针对不同类型或类的一组类似操作。

注意:模板在编译期展开,而非运行时确定参数的具体类型,因此会造成不同程度的代码膨胀。 STL是基于模板实现的,关于代码膨胀的讨论,可以参考知乎的一篇帖子“为何某些公司不允许使用 C++ STL?”, 里面观点各异,自行分辨。

下面我们对类模板和函数模板分别介绍。

1.1 类模板 class template

1.1.2 声明一个 类模板

类模板定义了一组针对不同类型的类,它也是参数化类型的一种。模板的变量可以是类型或特定类型的值。msdn 上给出了下面这个例子:

// class_templates.cpp
template <class T, int i> class TempClass 
{
public:
    TempClass( void );
    ~TempClass( void );
    int MemberSet( T a, int b );
private:
    T Tarray[i];
    int arraysize;
};

int main()
{
  // ... 省略代码 ...
}

上面这个例子中,模板 TempClass 接收两个参数:类型 T 和 整型数 i 。T 可以是 int、double等基本类型,也可以是结构体和类。 变量 i 作为一个整形常量传递给 TempClass。由于 i 是一个常量,在编译期就可以确定,所以你可以使用标准语法声明一个数组作为其成员变量。

1.1.3 类模板的特化

之前侯捷老师提到过,特化分为两种:个数上的“偏”和类型上的“偏”。下面我们对这两种类型一一讨论。 MSDN上的讲解十分详细, 这里我会使用上面的讲解和代码。

个数上的“偏”:通常情况下,模板有一个或多个类型参数,我们可以将某些类型参数固化,从而使其接受更少(或不接收)类型参数。 举个前面使用过的例子:

// 定义一个类模板,它使用一个模板作为模板参数
template <typename T,
  template <typename T>
  class Container
>
class XCls {
private:
  Container<T> c;
public:
  // ...
};

// 定义Lst
template<typename T>
using Lst = list<T, allocator<T> >;     // 注意: Lst 只有一个模板参数,而 list 有两个模板参数
// 使用该模板
int main() {
  XCls<string, list> mylist1;   // 合法的定义

//XCls<string, Lst> mylist2;    // 不合法,因为 XCls 的第二个模板参数只接受一个参数(有点绕,think about it)
}

上面这个例子中, Lst 就是 std::list 的一个偏特化版本。

类型上的“偏”:这种方式允许模板接收 指针、引用、函数指针等作为参数类型。我们看一个例子:

// https://msdn.microsoft.com/en-us/library/3967w96f.aspx
// partial_specialization_of_class_templates.cpp
template <class T> struct PTS {
   enum {
      IsPointer = 0,
      IsPointerToDataMember = 0
   };
};

template <class T> struct PTS<T*> {
   enum {
      IsPointer = 1,
      IsPointerToDataMember = 0
   };
};

template <class T, class U> struct PTS<T U::*> {
   enum {
      IsPointer = 0,
      IsPointerToDataMember = 1
   };
};

struct S{};

extern "C" int printf_s(const char*,...);

int main() {
   S s, *pS;
   int S::*ptm;
   printf_s("PTS<S>::IsPointer == %d PTS<S>::IsPointerToDataMember == %d\n", 
           PTS<S>::IsPointer, PTS<S>:: IsPointerToDataMember);
   printf_s("PTS<S*>::IsPointer == %d PTS<S*>::IsPointerToDataMember ==%d\n"
           , PTS<S*>::IsPointer, PTS<S*>:: IsPointerToDataMember);
   printf_s("PTS<int S::*>::IsPointer == %d PTS"
           "<int S::*>::IsPointerToDataMember == %d\n", 
           PTS<int S::*>::IsPointer, PTS<int S::*>:: 
           IsPointerToDataMember);
}

这段代码会打印出:

PTS<S>::IsPointer == 0 PTS<S>::IsPointerToDataMember == 0
PTS<S*>::IsPointer == 1 PTS<S*>::IsPointerToDataMember ==0
PTS<int S::*>::IsPointer == 0 PTS<int S::*>::IsPointerToDataMember == 1

msdn 还有三个偏特化的例子,都不错,有兴趣的花,点击“Partial specialization”

1.1.4 默认模板参数

不多说,参考 msdn 文档

1.2 函数模板 function template

函数模板,定义了一类行为。和类模板一样,在C++编译出的二进制文件中,不存在函数模板这个概念。 在编译期,在调用函数模板时,编译器会以某个类型作为参数生成一个特化的函数。 我们首先看个例子:

// https://msdn.microsoft.com/en-us/library/y2se4kz7.aspx
// function_templates1.cpp
template< class T > void MySwap( T& a, T& b ) {
   T c(a); 
   a = b; 
   b = c;
}

上面这个函数实现了两个变量值的交换(pass by reference)。

Part 2 偏特化

有时候,模板并不能满足所有特化类型的需求,所以需要针对某些类型指定特化版本。 在标准库中,典型的偏特化有std::vector 和 [std::vector](http://www.cplusplus.com/reference/vector/ "vector")。

我们再举一个例子,如果我们想覆盖上面 MySwap 针对 double 的特化函数,则可以像下面这样定义一个函数:

template<> void MySwap(double a, double b);

当一个函数调用匹配多个模板实例时,编译器会选择一个最佳匹配。如何去选择,下面我们讨论下这个主题。

2.1 函数模板的偏序 (partial ordering)

这部分内容的理解均来自于 msdn

这里我只取一部分自认为比较重要规则放到这里:

  1. A template specialization for a specific type is more specialized than one taking a generic type argument.
  2. A template taking only T* is more specialized than one taking only T, because a hypothetical type X* is a valid argument for a T template argument, but X is not a valid argument for a T* template argument.
  3. const T is more specialized than T, because const X is a valid argument for a T template argument, but X is not a valid argument for a const T template argument.
  4. const T* is more specialized than T, because const X is a valid argument for a T* template argument, but X* is not a valid argument for a const T* template argument.

下面这个例子也是从msdn上拷贝过来的,比较能体现 一般类型、指针特化、const 指针特化 的偏序:

// partial_ordering_of_function_templates.cpp
// compile with: /EHsc
#include <iostream>

extern "C" int printf(const char*,...);
template <class T> void f(T) {
   printf_s("Less specialized function called\n");
}

template <class T> void f(T*) {
   printf_s("More specialized function called\n");
}

template <class T> void f(const T*) {
   printf_s("Even more specialized function for const T*\n");
}

int main() {
   int i =0;
   const int j = 0;
   int *pi = &i;
   const int *cpi = &j;

   f(i);   // Calls less specialized function.
   f(pi);  // Calls more specialized function.
   f(cpi); // Calls even more specialized function.
   // Without partial ordering, these calls would be ambiguous.
}

上面这段代码会打印出:

Less specialized function called
More specialized function called
Even more specialized function for const T*

Part 3 STL 库的容器(类模板)

标准库中包含了不少优秀的类模板和函数模板,本文中列出一部分常用的容器:

  1. std::vector
  2. std::deque
  3. std::list
  4. std::stack
  5. std::queue
  6. std::map & std::multimap
  7. std::set & std::mutliset

关于这些容器的用法,上面已经给出了链接,需要的时候查文档即可。