1. 定义模板

有这样的一个需求,定义一个比较两个数大小的函数,但是这两个数的类型是不确定的,我们现在能想到的实现方法是定义多个重载的函数:

// 字符串版本的
int compare(const string &v1, const string &v2) {
    if (v1 < v2) return -1;
    if (v1 > v2) retrurn 1;
    return 0;
}
// 浮点类型
int compare(const double &v1, const double &v2) {
    if (v1 < v2) return -1;
    if (v1 > v2) retrurn 1;
    return 0;
}

以上这两个函数唯一的区别就是参数类型的不同,函数内部的实现完全一致。实际的应用场景中可能还不只上面两种类型,可能更多,很显然重载函数的方法是不可行的。这就要使用到本章的模板编程。

1.1 函数模板

针对以上的需求我们可以使用函数模板来实现,模板具体的代码如下

template<tyoename T>
int compare(const T &v1, const T &v2) {
    if (v1 < v2) return -1;
    if (v1 > v2) retrurn 1;
    return 0;
}

模板的定义以关键字template 开始,后跟一个模板参数列表

在模板定义中,模板参数列表不能为空

模板参数列表的作用类似于函数的参数列表,模板是把类型参数化。

实例化函数模板

当我们调用 compare 时编译器会实例化一个特定的函数,并且会自动的推导模板的类型参数

cout << compare(1, 0) << endl;
// 显示指定类型
cout << compare<int>(1, 0) << endl;

这里 T 模板被推导为 int 类型,即实例化的函数是:

int compare(const int& , const int&);

模板类型参数

模板函数的类型参数可以看作类型说明符,可以在模板函数的内部使用:

template<typename T>
T foo(T* p) {
    T tmp = *p;
    // ...
    return tmp;
}

模板列表可以有多个类型参数,用逗号分割开,但是每个类型参数前都需要加 typenameclass 关键字

template<typename T, U>  // 错误
T calc(const T&, const U&);  

// 正确
template<typename T, class U> 
T calc(const T&, const U&);  

typenameclass 在定义模板参数时,两者时完全等价的,没有任何区别。typename 是 C++11添加的,而 class 是之前标准中使用的,为了保持兼容性,class 也一直保留着。

非类型模板参数

除了类型参数,还可以在模板中定义非类型参数。一个非类型参数表示的是一个常量值

template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]) {
    return strcmp(p1, p2);
}

// 调用
compare("hi", "mmo");

编译器会使用字面常量的大小来代替 NM ,从而实例化模板。上面调用生成的函数实例:

// 字符串结尾有一个结束符
int compare(const char (&p1)[3], const char (&p2)[4]);

非类型模板参数的模板实参必须是常量表达式

inline 和 constexpr 的函数模板

函数模板可以声明为 inlineconstexpr , 这两个说明符放在模板参数列表之后,返回类型之前:

// 正确
template<typename T> inline T min(const T&, const T&);
// 错误
inline template<typename T> T min(const T&, const T&);

模板编译

当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。

通常,我们调用一个函数时,编译器只需要知道声明。类似的,使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此函数声明和或类定义放在头文中,而函数定义和类成员函数的定义放在源文件中。

模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,==模板的头文件通常既包括声明也包括定义。==

1.2 类模板

类模板 是用于生成类的蓝图。与函数模板的不同之处是,==编译器类模板不能为类模板推断模板参数类型。==

定义类模板

StrBlob 为例,实现它的模板版本Blob

template<typename T>
class Blob {
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type  size_type;
    //构造函数
    Blob();
    Blob(std::initializer_list<T> il);

    size_type size() const {return  data->size; }
    bool empty() const { return data->empty(); }
    void push_back(T&& t) { data->push_back(std::move(t)); }
    void pop_back();

    T& back();
    T& operator[](size_type i);

private:
    std::shared_ptr<std::vector<T>> data;
    void check(size_type i, const std::string &msg) const;
};

上面代码和12.1.1 小节的代码差不多,只是用 T 替换了 string

实例化类模板

模板的实例化需要显式的指定模板参数的类型,编译器不能推导

Blob<int> ia;   // 空 Blod<int>
Blob<int> ia={0, 1, 2, 3, 4};   // 有5个参数的 Blod<int>

对我们指定的每一种元素类型,编译器都生成一个不同的类:

Blod<string> names;
Blod<bouble> prices;

上面创建了两个不类型的实例。编译器会生成两个==完全独立==的类。

在模板作用域引用模板类型

一个实例化的类型总是包含类型参数, 但是如给在类模板种使用了另一个模板,我们可以使用一个模板参数传递给另一个模板:

 std::shared_ptr<std::vector<T>> data;

Blod 中 使用了类模板vector ,我们就是使用的Blod 的模板参数T 传递给 vector

类模板的成员函数

和普通的类成员函数一样,我们既可以在类内定义成员函数,也可以在在类外定义。但是需要注意在类外定义的格式:

template<typename T>
ret-type Blod<T>::func-name(parm-list) { /* */ }

// back()的定义
template<typename T>
T&  Blod<T>::back() {}

需要注意无论是函数模板还是类模板,声明和定义通常写在同一个文件(头文中)

check 和元素访问成员

check 的定义

template<typename T>
void Blob<T>::check(Blob::size_type i, const std::string& msg) const {
    if (i >= data->size()) {
        throw std::out_of_range(msg);
    }
}

其他成员函数的定义

template<typename T>
T& Blob<T>::back() {
    check(0, "back on empty Blod");
    return data->back();
}

template<typename T>
T& Blob<T>::operator[](size_type i) {
    check(i, "subscript out of range");
    return (*data)[i];
}

template<typename T>
void Blob<T>::pop_back() {
    check(0, "pop_back on empty Blod");
    data->pop_back();
}

类模板成员函数的实例化

==默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化==

// 实例化 Blod<int> 和  Blob(std::initializer_list<T> il);
Blod<int> squares = {0, 1, 2, 3, 4, 5}; 

其他没有被调用的成员函数都没有被实例化。

在类代码内简化模板类名的使用

模板类的使用必须跟上模板参数,但是有一个例外,即在自己的作用域内(类内),我们可以直接使用模板名而不提供模板实参

template<typename T>
class BlobPtr {
public:
    // ...
    BlobPtr& operator++();  // 前置++
};

上面的返回类型直接使用模板名称,而没有指定模板参数。如果是在类外定义还是需要指定模板实参的

template<typename T>
BlobPtr<T>& BlobPtr<T>::operator++() {
    //...
}