泛型编程中对类使用模板。

1. 简单使用类模板

以stack为例,展示类模板的特性,先写出一个stack的样例。与函数模板一样,类模板也需要在前面加上模板声明。需要注意的是,类的成员函数的实现可以不放在类中,放在外面也需要加上模板声明

template <typename T>
class stack{
private:
    vector<T> elems;
public:
    void push(T const& elem);
    void pop();
    T const& top();
    bool empty(){return elems.empty()};
};
template<typename T>
void stack<T>::push(T const& elem)
{
    elems.push_back(elem);
}
template<typename T>
void stack<T>::pop()
{
    assert(!elems.empty());
    elems.pop_back();
}
template<typename T>
T const& stack<T>::top()
{
    assert(!elems.empty());
    return elems.back();
}

我们可以继续为其添加拷贝构造函数和赋值运算符:

stack(stack<T> const&);
stack<T>& operator=(stack<T> const&);

通过上面这个例子,我们熟悉类类模板的一些特性。使用类模板时必须要 显式地特化模板参数(specify the template arguments explictly)

Stack< int>    intStack;// stack of ints
Stack<std::string> stringStack;  // stack of string

当然也可以使用usingtypedef来简化:

using IntStack = Stack<int>; 
typedef Stack<int> IntStack

类模板可以实现部分使用(partial usage),这意味着模板参数只需要提供必要的操作,而不是所有。下面这个例子中模板类stack中包含了一个printOn函数,而pair类型不能与printOn相适应,但我们依然可以创建并执行一些必要的操作,但不能使用printOn.(因为无法使用操作符<<

template<typename T> 
class Stack {
    ...
    void printOn(std::ostream&) const;
};

template <typename T>
void Stack<T>::printOn(std::ostream& os) const
{
    for (const T& x : v) os << x << ' ';
}

Stack<std::pair<int, int>> s; // std::pair没有定义operator<<
s.push({1, 2}); // OK
s.push({3, 4}); // OK
std::cout << s.top().first << s.top().second; // 34
s.printOn(std::cout); // 错误:元素类型不支持operator<<

类模板也可以使用默认实参。当然这并不意味着传入的参数被强制规定为std::vector,而是作为一种选择,<Stack> doubleStack;这样依然是ok的。

template<typename T, typename Cont = std::vector<T>>
class Stack { 
private:
    Cont v;
public: 
    void push(const T& x);
    void pop();
    const T& top() const;
    bool empty() const;
};

2. 友元

假设我们重载<<运算符来替代printOn函数:

template<typename T> 
class Stack {
    ...
    void printOn(std::ostream& os) const
    {
        for (const T& x : v) os << x << ' ';
    }
    friend std::ostream& operator<<(std::ostream& os, const Stack<T>& stack) {
        stack.printOn(os); 
        return os;
    }
};

但是,如果将友元的定义和实现放在类外就会出一些问题

template<typename T> 
class Stack {
    ...
    friend std::ostream& operator<<(std::ostream&, const Stack<T>);
};

std::ostream& operator<<(std::ostream& os,
    const Stack<T>& stack) // 错误:类模板参数T不可见
{
    stack.printOn(os);
    return os;
}

由于友元并不属于这个类,所以没法享受到类型推导的成果,自然就会报错。这个问题的解决方案有很多,我挑选了一种我认为比较好的办法:隐式声明一个新的函数模板,并使用不同的模板参数

template<typename T> 
class Stack {
    … 
    template<typename U> 
    friend std::ostream& operator<<(std::ostream&, const Stack<U>&);
};

// 类外定义
template<typename U>
std::ostream& operator<<(std::ostream& os, const Stack<U>& stack)
{
    stack.printOn(os);
    return os;
}

3. 特化与偏特化

特化(specify),可以直接指定模板的类型,标志是template<>。由于是直接指定的方式,所以声明模板类型时就不必要再添东西了。

template<>
class Stack<std::string> {
    std::deque<std::string> v;
public:
    void push(const std::string&);
    void pop();
    std::string const& top() const;
    bool empty() const;
};
void Stack<std::string>::push(const std::string& x)
{
    v.emplace_back(x);
}

在上面的例子中,我们直接指定stack为string类型。注意在类外定义的时候,也必须带上string。


偏特化(partial specify),在已经声明某一个模板的前提下,再对他进行声明。

(1)指定指针

#include "stack1.h" //非常重要

template<typename T>
class Stack<T*> {
    std::vector<T*> v;
public:
    void push(T*);
    T* pop();
    T* top() const;
    bool empty() const;
};

template<typename T>
void Stack<T*>::push(T* x)
{
    v.emplace_back(x);
}
...
Stack< int*> ptrStack; //用法展示
ptrStack.push(new int{42});

必须要在stack1.h中声明成如下形式,否则无法通过编译。

template<typename T,typename M>
class Stack {

};

(2)指定部分模板参数

template<typename T1, typename T2> 
class A
{};

// 偏特化:两个模板参数有相同类型
template<typename T> 
class A<T, T>
{};

// 偏特化:第二个模板参数类型为int
template<typename T> 
class A<T, int>
{};

// 偏特化:两个模板参数都是指针类型
template<typename T1, typename T2> 
class A<T1*, T2*>
{};

A<int, double> a; // A<T1, T2>
A<double, float> b; // A<T, T>
A<double, int> c; // A<T, int>
A<int*, double*> d; // A<T1*, T2*>

要注意避免歧义性错误:

A<int, int> e; // 错误:同时匹配A<T, T>和A<T, int>
A<int*, int*> f; // 错误:同时匹配A<T, T>和A<T1*, T2*>

4. 模板化聚合

当一个类的对象拥有另一个类的对象时就发生类聚合。这种聚合不依赖与继承、友元等关系存在。

struct C
{
  int a;
};

struct D
{
  double b;
  C c;
};

事实上,聚合类也能作为模板存在:

template<typename T> 
struct A {
    T x;
    std::string s;
};

这样可以为了参数化值而定义一个聚合:

A<int> a;
a.x = 42;
a.s = "initial value";