0. 什么是模板
C++中的模板(template),是泛型编程的基础。一个模板就是一个蓝图,用来创建类或函数的蓝图。
模板主要分为两类:
- 函数模板(function template)
- 类模板(class template)
本文主要介绍类模板(class template)。
关于模板的全部内容,参考【C++深陷】之“模板”。
1. 定义类模板
一个例子:
template <typename T>
class Blob {
public:
// 类型成员
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// 构造函数
Blob();
// 函数成员,定义在类内
size_type size() const { return data->size(); }
void push_back(const T &t) { data->push_back(t); }
// 函数成员,定义在类外
T &operator[](size_type i);
private:
// 数据成员
std::shared_ptr<std::vector<T>> data;
};
template
关键字不可以少,紧接模板参数列表。
在类中可以像使用其他类型一样使用T
,我们定义了一个data
,是指向vector<T>
类型的shared_ptr
智能指针。
使用类型别名技术,我们定义了两个类型成员,见第5节。
我们定义了一个构造函数,两个类内成员函数,一个类外成员函数,定义见第2节。
至于为什么size_type
类型别名这句话有一个typename
,见第8节。
2. 在类外定义函数成员
类内的函数成员直接定义即可,类外的按如下格式:
template <typename T>
Blob<T>::Blob()
: data(std::make_shared<std::vector<T>>())
{}
template <typename T>
T &Blob<T>::operator[](size_type i) {
return (*data)[i];
}
必须以template
开头,后接和类模板相同的模板参数列表。
接下来进入了Blob<T>
的作用域内,可以适当的省略,见第6节。
3. 类模板的实例化
在实例化类模板时,我们必须显示指定模板实参,即显示模板实参(explicit template argument)。
Blob<int> ia; // 此时T为int类型
上面代码编译之后,编译器重写Blob
模板,将每个T
都换成int
:
class Blob {
public:
// 类型成员
typedef int value_type;
typedef typename std::vector<int>::size_type size_type;
// 构造函数
Blob();
// ...
int &operator[](size_type i);
private:
// 数据成员
std::shared_ptr<std::vector<int>> data;
};
注意,上述代码仅仅是示例,并非每个函数成员都会被实例化,见第4节。
4. 成员函数的实例化
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。如果一个成员函数没有被使用,则它不会被实例化。
但是C++总会给你意外或惊喜,此时我们可以记住这个特性,但也要认识到除了默认情况,很多情况模板类的成员函数都被实例化了。
5. 类模板的类型别名
类型别名技术,可以通过typedef
和using
来是我们的程序可读性变强。
类模板也可以:
// 存储string专用的blob
// 此时必须使用具体类型而不能是T
typedef Blob<string> StrBlob;
严格意义上来说,不可以定义Blob<T>
的typedef
,但是可以定义一个模板的类型别名:
// twin是一个类型别名
// 但是是一个有模板实参的类型别名
template<typename T> using twin = pair<T, T>;
// typedef pair<T, T> twin
// 是不行的
// 这样使用
twin<int> pos;
6. 省略模板参数列表
在模板类自己的作用域内,可以省略模板参数列表,例如:
template <typename T>
class Blob {
public:
// ...省略
// 没有使用Blob<T>
Blob &operator=(const Blob &);
private:
// 数据成员
std::shared_ptr<std::vector<T>> data;
};
上面的赋值运算符没有使用Blob<T>
的形式。
在类外:
template <typename T>
Blob<T> &Blob<T>::operator=(const Blob &rhs) {
// ...
}
在遇到Blob<T>::
之后,都属于类的作用域,都可以不添加<T>
。
7. 类模板中的静态成员
类模板也可以包含静态成员:
template <typename T>
class Foo {
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
};
每个Foo实例都有其自己的static成员实例,Foo<X>
和Foo<U>
互不干扰。
在类外定义静态成员变量需要带上类的模板参数列表:
template <typename T>
size_t Foo<T>::ctr = 0; // 定义并初始化ctr
使用静态成员函数,可以用一下两种方式:
// 方式1
auto ct = Foo<int>::count();
// 方式2
Foo<double> df;
ct = df.count();
8. 如何区分类模板中的类型成员和静态成员
类的成员一共有5项:
- 类型成员
- 数据成员
- 函数成员
- 静态成员:静态成员变量、静态成员函数
- 成员模板(函数)
其中类型成员和静态成员都属于类本身,通过作用域运算符::
访问。
当我们定义了一个类模板,有模板参数T
。假设T是一个类类型,我们是可以访问T
中的类型成员和静态成员,此时我们无法区分开访问的是哪一个。比如:
// 是T中的静态变量s 乘 p
// 还是T中的类型成员定义了一个指针p
T::s * p;
C++默认访问的是静态数据,如果写成上面,就执行乘法。
可以通过typename
关键字显示指定我访问的是类型:
typename T::s *p = nullptr;
此处的typename
和模板参数列表中的typename功能不同,注意区分。
这也是为什么前例中Blob
的size_type
有一个typename
。
template <typename T>
class Blob {
public:
// 类型成员
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// ...
};
9. 总结
学会定义类模板、在类外定义类模板的成员函数,以及在类模板作用域内用到类模板名时可以适当省略。
在实例化类模板的时候,必须要指定模板实参。
可以使用类型别名技术,简化我们的定义。
使用typename
来区分类模板的静态成员和类型成员。