一、定义 C++ 类模板
template<typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(T const& ele);
void pop();
T const& top() const;
bool empty() const {
return elements.empty();
}
};
template<typename T>
void Stack<T>::push(const T &ele) {
elements.push_back(ele);
}
template<typename T>
void Stack<T>::pop() {
assert(!elements.empty());
elements.pop_back();
}
template<typename T>
T const & Stack<T>::top() const {
assert(!elements.empty());
return elements.back();
}
定义一个 C++ 类模板和定义一个函数模板类似,可以指定一个或者多个模板参数标识符。
在类外定义成员函数的实现时,需要带上模板参数标识符。
template<typename T> xxx Stack<T>::yyy() {}
定义构造函数、拷贝构造函数以及拷贝赋值函数时,不需要指定模板参数标识符。但是你也可以指定参数标识符,只不过效果上是等价的,看起来还显得冗余。
Stack(){}
Stack(Stack const& s) {
this->elements = s.elements;
}
Stack& operator=(Stack const & s) {
this->elements = s.elements;
return *this;
}
总的来说,基本原则就是,在只需要类名字而不需要参数类型时可以不指定模板参数标识符,需要参数类型时则必须带上模板参数标识符。
另外,与非模板类不同的是,模板不能定义在函数或者局部分作用域 {} 的内部,只能定义在全局 / 命名空间内。
二、模板类的使用
Stack<int> intStack;
Stack<float> floatStack;
Stack<char> charStack;
Stack<int *> intPointerStack;
Stack<Stack<int>> intStackStack;
定义好了模板之后,就可以把模板当作一个普通的类来使用了,比如可以用来定义变量。定义变量时,可以指定类型参数为基本的类型、类类型以及模板类型。模板参数还可以是指针,但不能是引用。为引用时,会得到下面的错误。
模板类型作为参数时,在 c++11 之前右边的尖括号必须有空格,否则 >> 会被语义解析为右移运算符。
Stack<Stack<int> > intStackStack;
除了用来定义变量,模板类还可以被当作函数参数,可传值、传引用 & 传指针。函数参数可被 const 以及 volatile 修饰。
总的来说,基本原则是,任意类型都可以作为模板参数。但对于模板如果有运算符的操作,那模板参数的实参类型也必须支持其所有的运算符操作。
三、模板类重载运算符
下面是一个重载输出的运算符实现,和非模板类一样,建议将运算法重载定义为友元函数。
void printOn(std::ostream& strm) {
for (T const & elem : elements) {
strm << elem << ' ';
}
}
friend std::ostream & operator<< (std::ostream& strm, Stack<T> const& s) {
s.printOn(strm);
return strm;
}
四、特化
特化是指对模板的参数定义一个特定类型的实现,对于被特化的模板,其成员变量和成员函数也都定义为特化实现。
template<>
class Stack<std::string> {
private:
std::vector<std::string> elements;
public:
void push(std::string const& ele);
std::string const& top() const;
void pop();
bool empty() const {
return elements.empty();
}
void printOn(std::ostream& strm) const{
for (std::string const & elem : elements) {
strm << elem << ' ';
}
}
friend std::ostream & operator<< (std::ostream& strm, const Stack<std::string> &s) {
s.printOn(strm);
return strm;
}
};
void Stack<std::string>::push(const std::string &ele) {
elements.push_back(ele);
}
std::string const& Stack<std::string>::top() const {
assert(!elements.empty());
return elements.back();
}
void Stack<std::string>::pop() {
assert(!elements.empty());
elements.pop_back();
}
使用特化类
// 将使用特化版本
Stack<std::string> stringStack;
stringStack.push("aaaa");
stringStack.push("bbbb");
std::cout << stringStack << std::endl;
template<> class Stack<std::string> {}; 可以看成全特化,虽然其只有一个模板参数。相对应的还有偏特化或者叫部分特化。
偏特化有两种情况,一种是模板参数仍然是存在的,而针对模板参数的指针做特化,比如
template<typename T> class Stack<T*> {};
另一种情况是多个模板参数的情况下,只特化其中一部分参数,比如
// 有如下原始模板类
template<typename T, typename U> class Stack {};
// 那么可以得到偏特化的模板类
template<typename T> class Stack<T,T> {};
template<typename T> class Stack<T, int> {};
template<typename T, typename U> class Stack<T* , U*> {};
五、类模板的默认参数与类模板别名
类模板的参数与函数模板一样,也可以指定默认参数。
template<typename T, typename Cont = std::vector<T>> class Queue {
private:
Cont elements;
public:
void push(T const& ele);
void pop();
T const& top() const;
};
类模板的别名是为了让模板使用起来更简单,可以通过 typedef 来定义,c++11 以后也可以通过 using 来定义。
typedef Stack<int> IntStack;
using LongStack = Stack<long>;
上面的两者是等价的,但 using 还可以用来定义别名模板,typedef 是不可以的。比如,下面的定义是合法的。
template<typename T>
using QueueStack = Queue<T, std::queue<T>>;
// 使用 QueueStack
QueueStack<int> intQueueStack;
但如果你用 typedef 来定义的话,就会得到如下的错误。
使用 using 和 typename 还可以定义类的成员别名。
template<typename T, typename Cont = std::vector<T>> class Queue {
private:
Cont elements;
public:
void push(T const& ele);
void pop();
T const& top() const;
public:
using Iterator = int;
// 可以这样定义一个成员别名模板
using MyIterator = typename Queue<T>::Iterator;
};
// 然后这样使用 MyIterator
Queue<int>::MyIterator it;
// 还可以这样定义成员别名模板
template<typename T>
using MyOuterIterator = typename Queue<T>::Iterator;
// 然后这样使用
MyOuterIterator<int> outer_it;
六、类模板的类型推导
从 c++17 开始允许类模板的类型推导,实现方式比如,可以通过实现一个带参的构造函数来推导。
explicit Stack(T &ele) : elements({ele}) {};