3 类模板
与函数相似, 类也可以被一种或多种类型参数化。
3.1 类模板Stack的实现
template <typename T>
class Stack {
private:
std::vector<T> elems; // 存储元素的容器
public:
void push(T const&); // 压入元素
void pop(); // 弹出元素
T top() const; // 返回栈顶元素
bool empty() const { // 返回栈是否为空
return elems.empty();
}
};
template <typename T>
void Stack<T>::push(T const& elem)
{
elems.push_back(elem); // 把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(); // 返回最后一个元素的拷贝
}
3.1.1 类模板声明
类模板的声明和函数模板的声明很相似: 在声明之前, 我们先(用一条语句) 声明作为类型参数的标识符; 我们继续使用T作为该标识符:
template <typename T>
class Stack {
//...
};
在此, 我们可以再次使用关键字class来代替typename:
template <class T>
class Stack {
//...
};
这个类的类型是Stack<T>, 其中T是模板参数。 因此, 当在声明中需要使用该类的类型时, 你必须使用Stack<T>。例如, 如果你要声明自己实现的拷贝构造函数和赋值运算符, 那么应该这样编写:
template <typename T>
class Stack {
//...
Stack(Stack<T> const&); //拷贝构造函数
Stack<T>& operator= (Stack<T> const&); //赋值运算符
//...
};
然而, 当使用类名而不是类的类型时, 就应该只用Stack; 譬如,当你指定类的名称、 类的构造函数、 析构函数时, 就应该使用Stack。
3.1.2 成员函数的实现
为了定义类模板的成员函数, 你必须指定该成员函数是一个函数模板, 而且你还需要使用这个类模板的完整类型限定符。 因此, 类型Stack<T>的成员函数push()的实现如下:
template <typename T>
void Stack<T>::push(T const& elem)
{
elems.push_back(elem); //把传入实参elem的拷贝附加到末端
}
3.2 类模板Stack的使用
为了使用类模板对象, 你必须显式地指定模板实参。 下面的例子展示了如何使用类模板Stack<>:
int main()
{
try {
Stack<int> intStack; // 元素类型为int的栈
Stack<std::string> stringStack; // 元素类型为字符串的栈
// 使用int栈
intStack.push(7);
std::cout << intStack.top() << std::endl;
// 使用string栈
stringStack.push("hello");
std::cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch(std::exception const& ex) {
std::cerr << "Exception: " << ex.what() << std::endl;
return EXIT_FAILURE; // 程序退出, 且带有ERROR标记
}
}
通过声明类型Stack<int>, 在类模板内部就可以用int实例化T。 因此, intStack是一个创建自 Stack<int>的对象, 它的元素储存于 vector,且类型为 int。 对于所有被调用的成员函数, 都会实例化出基于int类型的函数代码。
注意, 只有那些被调用的成员函数, 才会产生这些函数的实例化代码。 对于类模板, 成员函数只有在被使用的时候才会被实例化。 显然,这样可以节省空间和时间; 另一个好处是: 对于那些“未能提供所有成员函数中所有操作的”类型, 你也可以使用该类型来实例化类模板, 只要对那些“未能提供某些操作的”成员函数, 模板内部不使用就可以。例如,某些类模板中的成员函数会使用operator<来排序元素; 如果不调用这些“使用operator<的”成员函数, 那么对于没有定义operator<的类型,也可以被用来实例化该类模板。另一方面, 如果类模板中含有静态成员, 那么用来实例化的每种类型, 都会实例化这些静态成员。
借助于类型定义, 你可以更方便地使用类模板:
typedef Stack<int> IntStack;
void foo(IntStack const& s) //s是一个int栈
{
IntStack istack[10]; //istack是一个含有10个int栈的数组
//...
}
C++的类型定义只是定义了一个“类型别名”, 并没有定义一个新类型。 因此, 在进行类型定义:
typedef Stack<int> IntStack
之后, IntSatck和Stack<int>实际上是相同的类型, 并可以用于相互赋值。
3.3 类模板的特化
你可以用模板实参来特化类模板。 和函数模板的重载类似, 通过特化类模板, 你可以优化基于某种特定类型的实现, 或者克服某种特定类型在实例化类模板时所出现的不足。 另外, 如果要特化一个类模板, 你还要特化该类模板的所有成员函数。 虽然也可以只特化某个成员函数, 但这个做法并没有特化整个类, 也就没有特化整个类模板。
为了特化一个类模板, 你必须在起始处声明一个 template<>, 接下来声明用来特化类模板的类型。 这个类型被用作模板实参, 且必须在类名的后面直接指定:
template<>
class Stack<std::string>
{
...
}
...
进行类模板的特化时, 每个成员函数都必须重新定义为普通函数,原来模板函数中的每个T也相应地被进行特化的类型取代:
void Stack<std::string>::push(std::string const& elem)
{
elems.push_back(elem); //附加传入实参elem的拷贝
}
3.4 局部特化
类模板可以被局部特化。 你可以在特定的环境下指定类模板的特定实现, 并且要求某些模板参数仍然必须由用户来定义。 例如类模板:
template <typename T1, typename T2>
class MyClass
{
//...
};
就可以有下面几种局部特化:
//局部特化: 两个模板参数具有相同的类型
template <typename T>
class MyClass < T, T >
{
...
};
//局部特化: 第2个模板参数的类型是int
template<typename T>
class MyClass < T, int >
{
...
};
//局部特化: 两个模板参数都是指针类型。
template<typename T1, typename T2>
class MyClass < T1*, T2* >
{
...
};
下面的例子展示各种声明会使用哪个模板:
Myclass<int, float> mif; //使用MyClass<T1,T2>
MyClass<float, float> mff; //使用MyClass<T,T>
MyClass<float, int> mfi; //使用MyClass<T,int>
MyClass<int*, float*> mp; //使用MyClass<T1*,T2*>
如果有多个局部特化同等程度地匹配某个声明, 那么就称该声明具有二义性:
MyClass<int,int> m; //错误:同等程度地匹配MyClass<T,T>
// 和MyClass<T,int>
MyClass<int*,int*> m; //错误:同等程度地匹配MyClass<T,T>
// 和MyClass<T1*,T2*>
为了解决第2种二义性, 你可以另外提供一个指向相同类型指针的特化:
template<typename T>
class MyClass < T*, T* >
{
...
};
3.5 缺省模板参数
对于类模板, 你还可以为模板参数定义缺省值; 这些值就被称为缺省模板实参; 而且, 它们还可以引用之前的模板参数。 例如, 在类Stack<>中, 你可以把用于管理元素的容器定义为第2个模板参数, 并且使用std::vector<>作为它的缺省值:
template <typename T, typename CONT = std::vector<T> >
class Stack
{
private:
CONT elems; // 包含元素的容器
public:
void push(T const&); // 压入元素
void pop(); // 弹出元素
T top() const; // 返回栈顶元素
bool empty() const
{ // 返回栈是否为空
return elems.empty();
}
};
template <typename T, typename CONT>
void Stack<T, CONT>::push(T const& elem)
{
elems.push_back(elem); // 把传入实参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(); // 返回末端元素的拷贝
}
可以看到: 我们的类模板含有两个模板参数, 因此每个成员函数的定义都必须具有这两个参数。如果你只传递第一个类型实参给这个类模板, 那么将会利用vector来管理stack的元素;另外, 当在程序中声明Stack对象的时候, 你还可以指定容器的类型;
// int栈:
Stack<int> intStack;
// double栈, 它使用std::deque来管理元素
Stack<double,std::deque<double> > dblStack;
3.6 小结
•类模板是具有如下性质的类: 在类的实现中, 可以有一个或多个类型还没有被指定。
•为了使用类模板, 你可以传入某个具体类型作为模板实参; 然后编译器将会基于该类型来实例化类模板。
•对于类模板而言, 只有那些被调用的成员函数才会被实例化。
•你可以用某种特定类型特化类模板。
•你可以用某种特定类型局部特化类模板。
•你可以为类模板的参数定义缺省值, 这些值还可以引用之前的模板参数。