C++templates第二章类模板

和函数类似,类也可以被一个或多个类型参数化,容器类就是典型的一个例子,它可以被用来处理某一指定类型的元素,通过使用类模板,你也可以实现适用于多种类型的容器类,本章用一个stack的例子来展示:

Stack类模板的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <vector> 
#include <cassert>
template<typename T>
class Stack
{
private:
std::vector<T> elems; // elements
public:
void push(T const& elem); // push element
void pop(); // pop element
T const& top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
template<typename T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
template<typename T>
void Stack<T>::pop ()
{
assert(!elems.empty());
elems.pop_back(); // remove last element
}
template<typename T>
T const& Stack<T>::top () const
{
assert(!elems.empty());
return elems.back(); // return copy of last element
}

声明一个类模板

声明类模板和声明函数模板类似:在开始定义具体内容之前,需要先声明一个或者多个作为模板的类型参数的标识符。同样地,这一标识符通常用 T 表示:

1
2
3
4
5
6
7
8
9
template<typename T> 
class Stack {
// …
};
//在这里,同样可以用关键字 class 取代 typename:
template<class T>
class Stack {
// …
};

在类模板内部,T 可以像普通类型一样被用来声明成员变量和成员函数。在这个例子中,T 被用于声明 vector 中元素的类型,用于声明成员函数 push()的参数类型,也被用于成员函数 top 的返回类型:

这个类的类型是 Stack, 其中 T 是模板参数。在将这个 Stack类型用于声明的时候, 除非可以推断出模板参数的类型,否则就必须使用 Stack(Stack 后面必须跟着)。不过,如果在类模板内部使用 Stack 而不是 Stack,表明这个内部类的模板参数类型和模板类的参数类型相同(细节请参见 13.2.3 节)。

比如,如果需要定义自己的复制构造函数和赋值构造函数,通常应该定义成这样:

1
2
3
4
5
6
template<typename T>
class Stack
{ //…
Stack (Stack const&); // copy constructor
Stack& operator= (Stack const&); // assignment operator
};

与如下定义等效:

1
2
3
4
5
6
template<typename T> 
class Stack {
//…
Stack (Stack<T> const&); // copy constructor
Stack<T>& operator= (Stack<T> const&); // assignment operator
};

但是如果在类模板的外面,就需要这样定义:

1
2
emplate<typename T> 
bool operator== (Stack<T> const& lhs, Stack<T> const& rhs);

成员函数实现

定义类模板的成员函数时,必须指出它是一个模板,也必须使用该类模板的所有类型限制。 因此,要像下面这样定义 Stack的成员函数 push():

1
2
3
4
5
6
template<typename T> 
void Stack<T>::push (T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}

再考虑一个back

1
2
3
4
5
6
7
template<typename T> 
T const& Stack<T>::top () const
{
assert(!elems.empty());
return elems.back(); // return copy of last element
}

类模板的使用

直到c++17,在使用类模板的时候都需要显式指明模版参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stack1.hpp" 
#include <iostream>
#include <string>
int main() {
Stack< int> intStack; // stack of ints
Stack<std::string> stringStack; // stack of strings
// manipulate int stack
intStack.push(7);
std::cout << intStack.top() << ’\n’;
// manipulate string stack
stringStack.push("hello");
std::cout << stringStack.top() << ’\n’;
stringStack.pop();
}

注意,模板函数和模板成员函数只有在被调用的时候才会实例化。这样一方面会节省时间和 空间,同样也允许只是部分的使用类模板,我们会在 2.3 节对此进行讨论。

在 C++11 之前,在两个相邻的模板尖括号之间必须要有空格,如果你不这样做,>>会被解析成调用>>运算符,这会导致语法错误:

Stack<Stack< int>> intStackStack; // ERROR before C++11

友元

相比于通过 printOn()来打印 stack 的内容,更好的办法是去重载 stack 的 operator<<运算符。 而且和非模板类的情况一样,operator<<应该被实现为非成员函数,在其实现中可以调用 printOn():

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T> 
class Stack
{
void printOn() (std::ostream& strm) const
{
//...
}
friend std::ostream& operator<< (std::ostream& strm, Stack<T> const& s)
{
s.printOn(strm); return strm;
}
};

然而如果你试着先声明一个友元函数,然后再去定义它,情况会变的很复杂。

特例化

可以对类模板的某一个模板参数进行特化。和函数模板的重载(参见 1.5 节)类似,类模板 的特化允许我们对某一特定类型做优化,或者去修正类模板针对某一特定类型实例化之后的 行为。不过如果对类模板进行了特化,那么也需要去特化所有的成员函数。虽然允许只特例 化模板类的一个成员函数,不过一旦你这样做,你就无法再去特化那些未被特化的部分了。

为了特化一个类模板,在类模板声明的前面需要有一个 template<>,并且需要指明所希望特 化的类型。这些用于特化类模板的类型被用作模板参数,并且需要紧跟在类名的后面:

1
2
3
4
5
template<> 
class Stack<std::string>
{
//…
};

部分特例化

类模板可以只被部分的特例化。这样就可以为某些特殊情况提供特殊的实现,不过使用者还 是要定义一部分模板参数。比如,可以特殊化一个 Stack<>来专门处理指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stack1.hpp" 
// partial specialization of class Stack<> for pointers:
template<typename T>
class Stack<T*>
{
Private:
std::vector<T*> elems; // elements
public:
void push(T*); // push element
T* pop(); // pop element
T* top() const; // return top element
bool empty() const;
};

template<typename T>
void Stack<T*>::push (T* elem)
{
elems.push_back(elem); // append copy of passed elem
}

多模版参数的部分特例化

1
2
3
4
template<typename T1, typename T2> class MyClass 
{
//…
};

如下特例化都是可以的:

1
2
3
4
5
6
7
8
9
// partial specialization: both template parameters have same type 
template<typename T>
class MyClass<T,T> { … };
// partial specialization: second type is int
template<typename T>
class MyClass<T,int> { … };
// partial specialization: both template parameters are pointer types
template<typename T1, typename T2>
class MyClass<T1*,T2*> { … };

如果有不止一个特例化的版本可以以相同的情形匹配某一个调用,说明定义是有歧义的

默认类模版参数

和函数模板一样,也可以给类模板的模板参数指定默认值。比如对 Stack<>,你可以将其用 来容纳元素的容器声明为第二个模板参数,并指定其默认值是 std::vector<>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T, typename Cont = std::vector<T>> 
class Stack
{
private:
Cont elems; // elements
public:
void push(T const& elem); // push element
void pop(); // pop element T
const& top() const; // return top element
bool empty() const {
// return whether the stack is empty
return elems.empty();
}
};

类型别名

  • 使用关键字typedef

    typedef Stack<int> IntStack; // typedef

  • 使用关键字using

    using IntStack = Stack <int>; // alias declaration

类型推导

C++17之后,使用类模板时不一定需要显示指出所有的模版参数的类型了:

1
2
3
Stack< int> intStack1; // stack of strings 
Stack< int> intStack2 = intStack1; // OK in all versions
Stack intStack3 = intStack1; // OK since C++17