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
当然也可以使用using
或typedef
来简化:
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";