第三章 类模板
与函数相似,类也可以被一种或多种类型参数化。例如容器类,它们通常被用于管理某种特定的元素。只要使用类模板,就可以实现容器类,而不需要确定容器中元素的类型。
3.1 类模板 Stack 的实现
一个简单的类模板 Stack 实现如下:
#include <iostream>
#include <stdexcept>
template<typename T>
class Stack{
public:
void push(T const&);
void pop();
T top() const;
bool empty() const{ elems.empty(); }
private: std::vector<T> elems;
};
template<typename T>
void Stack<T>::push(T const& elem){
elems.emplace_back(elem);
}
template<typename T>
void Stack<T>::pop(){
if(elems.empty()) throw std::out_of_range("Stack<>::pop: empty stack");
elems.pop_back();
}
template<typename T>
T Stack<T>::top() const{
if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty stack");
return elems.back();
}
C++上述的类模板 Stack<> 使用的是 C++ 标准库中的类模板 vector<> 来实现的,这样做的好处就是不需要亲自实现内存管理、拷贝构造函数和赋值运算符。
3.1.1 类模板的声明
类模板的声明和函数模板的声明基本相似,即在声明之前,先声明作为类型参数的标识符:
- 使用
typename
关键字:
template<typename T>
class Stack{ };
C++- 使用
class
关键字:
template<class T>
class Stack{ };
C++在类模板的内部, T
可以像其他任何类型一样,用于声明成员变量和成员函数,在下面的例子中:
T
被用于声明std::vector
的元素类型- 声明
push()
是一个接受const T&
为唯一实参的成员函数 - 声明
top()
是返回类型T
的成员函数
template<typename T>
class Stack{
public:
Stack();
void push(T const&);
void pop();
T top() const;
private: std::vector<T> elems;
};
C++这个类的类型是 Stack<T>
,其中 T
是模板参数。因此,当在声明中需要使用该类的类型时,就必须使用 Stack<T>
, 例如要声明自己实现的拷贝构造函数和赋值运算符:
template<typename T>
class Stack{
/* copy constructor */
Stack(const Stack<T>&);
/* copy asssignment */
Stack<T>& operator=(const Stack<T>&);
}
C++然而,当使用类名而不是类的类型时,就应该只用 Stack
,例如:指定类的名称、类的构造函数、析构函数。
3.1.2 成员函数的实现
为了定义类模板的成员函数,就需要指定该成员函数一个函数模板,而且还使用这个类模板的完整类型限定符。
- 类型
Stack<T>
的成员函数push()
的实现如下:
template<typename T>
void Stack<T>::push(const T& elem){
elems.emplace_back(elem);
}
C++- 类型
Stack<T>
的成员函数pop()
的实现如下:
template<typename T>
T Stack<T>::pop(){
if(elems.empty()) throw std::out_of_range("Stack<>::pop: empty Stack");
/* leave out Exception Security */
T elem = elems.back();
elems.pop_back();
return elem;
}
C++- 类型
Stack<T>
的成员函数top()
的实现如下:
template<typename T>
T Stack<T>::top const{
if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty Stack");
return elems.back();
}
C++当然也可以直接将类模板的成员函数直接实现为内联函数,将其在类中实现:
template<typename T>
class Stack{
..
void push(const T& elem) { elems.push_back(elem); }
..
};
C++3.2 类模板的 Stack 的使用
#include <iostream>
#include <stdexcept>
template<typename T>
class Stack{
public:
void push(T const&);
void pop();
T top() const;
bool empty() const{ elems.empty(); }
private: std::vector<T> elems;
};
template<typename T>
void Stack<T>::push(T const& elem){
elems.emplace_back(elem);
}
template<typename T>
void Stack<T>::pop(){
if(elems.empty()) throw std::out_of_range("Stack<>::pop: empty stack");
elems.pop_back();
}
template<typename T>
T Stack<T>::top() const{
if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty stack");
return elems.back();
}
int main(){
try{
Stack<int> intStack;
Stack<std::string> stringStack;
intStack.push(7);
std::cout << intStack.top() << '\n';
stringStack.push("hello");
std::cout << stringStack.top() << '\n';
stringStack.pop();
stringStack.pop(); /* error */
}catch(cosnt std::exception& ex){
std::cerr << "Exception: " << ex.what() << '\n';
return EXIT_FAILURE;
}
}
C++通过声明类型 Stack<int>
,在类模板内部就可以用 int
实例化 T
,因此, intStack
是一个创建自 Stack<int>
的对象, 它的元素存储于 'std::vector',且类型为 int,对于所有被调用的成员函数,都会实例化出基于 int
类型的函数,同理 Stack<std::string>
也如此。
- 注:
- 只有那些被调用的成员函数,才会产生这些函数的实例化代码。
- 对于类模板,成员函数只有在被使用的时候才会被实例化。
- 这样做的好处:
- 节省空间和时间。
- 对于那些“未能提供所有成员函数中所有操作的”类型,也可以使用该类型来实例化类模板,只要在模板内部不使用就可以。
- 补充:什么是“未能提供某些操作的”成员函数:
operator<
operator==
operator()
- ......
- 如果在类模板中这些操作都未被实现,那么就无法使用(这些操作的默认实现无法应用在当前的模板类中)。
在上面的例子中,缺省构造函数、 push()
和top()
都被实例化了一个 int
实例和 std::string
版本,而 pop()
仅被实例化了一个 std::string
版本。
此外,如果类模板中含有某些静态成员,那么用来实例化的每种类型,都会实例化这些静态成员。
可以像使用其他任何类型一样使用实例化后的类模板类型,只要它支持所调用的操作就可以:
void foo(const Stack<int>& s){
Stack<int> istack[10];
...
}
typedef Stack<int> IntStack;
void foo(const IntStack& s){
IntStack istack[10];
}
C++使用 typedef
只是为 Stack<int>
取了一个“类型别名”,并没有定义一个新的类型。
因此,在定义了新的类型后:
typedef Stack<int> IntStack;
C++IntStack
和 Stack<int>
仍是相同的类型,并可用于相互赋值。 模板实参可以是任何类型:
- 浮点型指针:
Stack<float*>
int
类型的栈:Stack<Stack<int>>
唯一的要求就是:该类型必须提供被调用的所有操作,即所有操作符如==
, <
和>
都需要被重载。
3.3 类模板的特化
和函数模板重载类似,可以用模板实参来特化类模板,通过特化类模板,可以优化基于某种特定类型的实现,或者解决某种特定类型在实例化模板时所出现的问题。
另外,如果要特化一个类模板,就还需要特化该类模板的所有成员函数,尽管也可以只特化某个成员函数,但这个做法并没有特化整个类,也就没有特化整个类模板。
为了特化一个类模板,需要在起始处声明一个 template<>
,再声明用来特化类模板的类型。这个类型被用作模板实参,且必须在类名的后面直接指定:
template<>
class Stack<std::string>{
...
};
...
C++进行类模板的特化时,每个成员函数必须重新定义为普通函数,原来的模板函数中的每个 T 也相应地被进行特化的类型取代:
template<typename T>
class Stack{
public:
/* declare functions */
void push(T const&);
void pop();
T top() const;
bool empty() const{ return elems.empty(); }
private:
std::vector<T> elems;
};
/* implement functions */
template<typename T>
void Stack<T>::push(T const& elem){
elems.push_back(elem);
}
template<typename T>
void Stack<T>::pop(){
if(elems.empty()) throw std::out_of_range("Stack<>::pop(): empty stack");
elems.pop_back();
}
template<typename T>
T Stack<T>::top() const{
if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty Stack");
return elems.back();
}
/* a specified template for std::stirng */
template<>
class Stack<std::string>{
public:
void push(const std::string&);
void pop();
std::string top() const;
bool empty() const { return elems.empty(); }
private: deque<std::string> elems;
}
void Stack<std::string>::push(const std::string elem){
elems.emplace_back(elem);
}
void Stack<std::string>::pop(){
if(elems.empty()) std::out_of_range("Stack<std::string>::pop(): empty Stack");
elems.pop_back();
}
std::string Stack<std::string>::top const{
if(elems.empty()) std::out_of_range("Stack<std::string>::top(): empty Stack");
return elems.back();
}
C++上面的例子说明了,特化的实现可以和基本类模板(primary template)的实现完全不同。
3.4 局部特化
类模板可以被局部特化,可以在特定的环境下指定类模板的特定实现,并且要求某些参数仍需用户自己定义,例如类模板:
template<typename T1, typename T2>
class MyClass{
...
};
C++由上可以得到其他的几种局部特化:
- 局部特化:两个模板参数具有相同的类型
template<typename T>
class MyClass<T, T>{
...
};
C++- 局部特化:第二个模板参数是其他类型,例如:
int
template<typename T>
class MyClass<T, int>{
...
};
C++- 局部特化:两个模板参数都是指针类型
template<typename T>
class MyClass<T1*, T2*>{
...
};
C++- 特化后的类模板例子:
MyClass<int, float> mif; // use MyClass<T1,T2>, two different types
MyClass<float, float> mff; // use MyClass<T,T>, two same types
MyClass<float, int> mfi; // use MyClass<T, int>, specified the second param with int
MyClass<int*, float*> mp; // use MyClass<T1*, T2*>, two different types pointer
C++如果多个局部特化同等程度地匹配某个声明,那么就称该声明具有二义性:
MyClass<int, int> m; // error: as same as this matches MyClass<T, T> and MyClass<T, int>
MyClass<int*, int*> m; // error: as same as this matches Myclass<T, T> and MyClass<T1*, T2*>
C++解决第二种二义性,可以另外提供一个指向相同类型指针的特化:
template<typename T>
class MyClass<T*, T*>{
...
};
C++3.5 缺省模板参数
对于类模板,还可以为模板参数定义缺省值,这些值被称为缺省模板实参,而且,它们还可以引用之前的模板参数。
例如,在类 Stack<>
中,可以把用于管理元素的容器定义为第 2 个模板参数,并且使用 std::vector<>
作为它的缺省值:
#include <vector>
#include <stdexcept>
template<typename T, typename CONT = std::vector<T>>
class Stack{
public:
void push(const T&);
void pop();
T top() const;
bool empty() const { return elems.empty(); }
private:
// equivalent to
// typedef
// std::vector<int /* or other specific type */>
// CONT
CONT elems;
};
template<typename T, typename CONT>
void Stack<T, CONT>::push(const T& elem){
elems.emplace_back(elem);
}
template<typename T, typename CONT>
void Stack<T, CONT>::pop(){
if(elems.empty()) throw std::out_of_range("Stack<>::pop(): empty Stack");
elems.pop_back();
}
template<typename T, typename CONT>
T Stack<T, CONT>::top() const{
if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty Stack");
return elems.back();
}
C++在上面的代码中,类模板含有两个模板参数,因此每个成员函数的定义都必须具有这两个参数:
template<typename T, typename CONT>
void Stack<T, CONT>::push(const T& elem){
elems.emplace_back(elem);
}
C++如果只传递第一个类型实参给这个类模板,那么将会将会利用vector
来管理stack
的元素:
template<typename T,
typename CONT =
std::vector<T>>
class Stack{
...
private:
CONT elems;
...
};
C++当在程序中声明 Stack 对象的时候,还可以指定容器的类型:
#include <iostream>
#include <stdexcept>
#include <deque>
#include <cstdlib>
#include <vector>
template<typename T,
typename CONT = std::vector<T>>
class Stack{
public:
void push(const T&);
void pop();
T top() const;
bool empty() { return elems.empty(); }
private: CONT elems;
};
template<typename T, typename CONT>
void Stack<T, CONT>::push(const T& elem){
elems.emplace_back(elem);
}
template<typename T, typename CONT>
void Stack<T, CONT>::pop(){
if(elems.empty()) throw std::out_of_range("Stack<>::pop(): empty Stack");
elems.pop_back();
}
template<typename T, typename CONT>
T Stack<T, CONT>::top() const{
if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty Stack");
return elems.back();
}
int main(){
try{
Stack<int> intStack;
Stack<double, std::deque<dobule>> dblStack;
intStack.push(7);
std::cout << intStack.top() << '\n';
intStack.pop();
dblStack.push(42.42);
std::cout << dblStack.top() << '\n';
dblStack.pop();
dblStack.pop();
}catch(const std::exception& ex){
std::cerr << "Exception: " << ex.what() << '\n';
return EXIT_FAILURE;
}
}
C++- 使用
Stack<double, std::deque<double>>
可以声明一个“元素类型为double, 并且使用std::deque<>
在内部管理元素”的栈。
3.6 小结
- 类模板是具有如下性质的类:在类的实现中,可以有一个或多个类型还没有被指定。
- 为了使用类模板,可以传入某个具体类型作为模板实参,然后编译器将会基于该类型来实例化类模板。
- 对于类模板而言,只有那些被调用的成员函数才会被实例化。
- 可以用某种特定类型特化类模板。
- 可以用某种特定类型局部特化类模板。
- 可以为类模板的参数定义缺省值,这些值还可以引用之前的模板参数。