类模板是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数的类型。所以我们需要在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
我们将实现一个名为 Blob 的模板类,且该模板类会对元素的共享(且核查过的)访问能力。
类似函数模板,我们的类以关键字 template 开始,后跟模板参数列表。在类模板(及其成员)的定义中,我们将模板参数当做替身,代替使用模板时用户需要提供的类型或值:
template <typename T> class Blob {
public:
typedef T value_type;
// 为什么要使用 typename 在后面一篇博客有解释。
typedef typename std::vector<T>::size_type size_type;
// 构造函数
Blob();
Blob(std::initializer_list<T> i1);
// Blob 中的元素数目
size_type size() const { return data -> size(); }
bool empty() const { return data -> empty(); }
// 添加和删除元素
void push_back(const T& t) { data -> push_back(t); }
void push_back(T&& t) { data -> push_back(std::move(t)); }
void pop_back();
// 元素访问
T& back();
T& operator[](size_type i);
private:
std::shared_ptr<std::vector<T>> data;
void check(size_type i,const std::string &msg) const;
};
我们的 Blob 模板有一个名为 T 的模板类型参数,用来表示 Blob 保存的元素的类型。当用户实例化 Blob 时,T 就会被替换为特定的模板实参类型。
当我们使用类模板时,我们必须提供额外的信息。这些额外信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类。
例如:
Blob<int> id; // 空 Blob
Blob<int> ia2 = {0,1,2,3,4}; // 含 5 个元素的 Blob
对于 Blob
template <> class Blob<int> {
public:
typedef typename std::vector<int>::size_type size_type;
// 构造函数
Blob();
Blob(std::initializer_list<int> i1);
// ...
int& back();
int& operator[](size_type i);
private:
std::shared_ptr<std::vector<int>> data;
void check(size_type i,const std::string &msg) const;
};
实际上就是重写 Blob 模板,将模板参数 T 每个实例替换为给定的模板实参,在本例中就是 int。
为了阅读模板类代码,应该记住类模板的名字不是一个类型名。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
可能令人迷惑的是,一个类模板中的代码如果使用了另一个模板,通常不将一个实际类型(或值)的名字用作其模板实参。相反,我们通常将模板自己的参数当做被使用模板的参数。如,我们的 data 定义如下:
std::shared_ptr<vector<T>> data;
它使用了 Blob 的类型参数来声明 data 是一个 shared_ptr 实例。当我们实例化一个特定类型的 Blob,例如 Blob
shared_ptr<vector<string>> data;
我们既可以在类模板内部,也可以在类模板外部为其定义函数,且定义在类模板内的成员函数被隐式声明为内联函数。
类模板的每个实例都有自己版本的成员函数。因而,定义在类外部的函数必须以关键字 template 开始,后接类模板参数列表。同时,定义时我们仍然需要说明成员属于哪个类,所以 Blob 类外部定义成员的格式应该是下面这样:
template<typename T>
ret-type Blob<T>::member-name(parm-list)
首先定义 check 成员,它检查一个给定的索引:
template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const {
if(i >= data -> size())
throw std::out_of_range(msg);
}
我们可以发现,除了声明作用域的类名是使用了模板参数列表外,其他的几乎和普通函数一样。
我们也可以写出 back 函数和下标运算符:
template <typename T>
T& Blob<T>::operator[](size_type i) {
check(i,"subscript out of range");
return (*data)[i];
}
template <typename T>
T& Blob<T>::back() {
check(0,"back on empty Blob");
return data -> back();
}
模板返回的是 T&,指向用来实例化 Blob 的类型。
pop_back 函数:
template <typename T>
void Blob<T>::pop_back() {
check(0,"pop_back on empty Blob");
data -> pop_back();
}
同样,构造函数在类外的定义和普通函数一样,要以 template 关键字开始:
template <typename T>
Blob<T>::Blob():data(std::make_shared<std::vector<T>>()) { }
template <typename T>
Blob<T>::Blob(std::initializer_list<T> i1):data(std::make_shared<std::vector<T>>(i1)) { }
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。例如,下面代码:
// 实例化 Blob 和接受 initializer_list的构造函数
Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};
// 实例化 Blob::size() const
for(size_t i = 0;i != squares.size();++ i)
square[i] = i * i; // 实例化 Blob::operator[](size_t)
如果一个成员函数没有被使用,则它不会被实例化。 成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
当我们使用一个类模板类型时必须提供模板参数,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供模板实参:
// 若试图访问一个不存在的元素,BlobPtr 将抛出异常
// 当然,我们的 BlobPtr 应该为 Blob 的友元类。
// 声明方式:template class BlobPtr;
template <typename T> class BlobPtr {
public:
BlobPtr(): curr(0) { }
BlobPtr(Blob<T> &a,size_t sz = 0):
wptr(a.data), curr(sz) { }
T& operator*() const {
auto p = check(curr,"dereference past end");
return (*p)[curr];
}
BlobPtr&operator++(); // 前置
BlobPtr&operator--();
BlobPtr&operator++(int); // 后置
BlobPtr&operator--(int);
private:
std::size_t curr; // 数组中的当前位置
std::weak_ptr<std::vector<T>> wptr;
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
std::shared_ptr<std::vector<T>>
check(std::size_t t,const std::string&msg) const;
};
template<typename T>
std::shared_ptr<std::vector<T>> BlobPtr<T>::check(std::size_t t,const std::string &msg) const {
auto ret = wptr.lock();
if(!ret) throw std::runtime_error("unbound StrBlobPtr");
if(t >= ret -> size())
throw std::out_of_range(msg);
return ret;
}
我们可以发现,BlobPtr 的前置递增和递减返回的是 BlobPtr&,而不是 BlobPtr
当我们在类模板外定义其成员时,必须记住,我们不在类的作用域中,只要遇到类名才表示进入类的作用域:
// 前置递增/递减
template <typename T> BlobPtr<T>& BlobPtr<T>::operator++() {
check(curr,"increment past end of BlobPtr");
++ curr;
return *this;
}
template <typename T> BlobPtr<T>& BlobPtr<T>::operator--() {
-- curr; // 如果 curr 为 0,curr 是 size_t 类型,它会变成一个极大数
check(curr,"decrement past begin of BlobPtr");
return *this;
}
当一个类包含一个友元声明时,类与友元各自是否是模版是无关的。如果一个模板类包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
类模板与另一个模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。例如,我们为 Blob 定义 BlobPtr 友元,为 Blob 定义 operator== 友元:
template<typename T> class BlobPtr;
template<typename T> class Blob;
template<typename T>
bool operator==(const Blob<T>&,const Blob<T>&);
template<typename T> class Blob {
// 每个 Blob 实例将访问权限授予用相同类型实例化的 BlobPtr 与相等运算符
friend class BlobPtr<T>;
friend bool operator==<T>(const Blob<T>&,const Blob<T>&);
// 其他的与之前相同
};
在上面代码中,每个 Blob 实例将访问权限授予用相同类型实例化的 BlobPtr 与相等运算符。如:
Blob<int> ia; // BlobPtr 和 operator== 都是本对象的友元
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元:
// 前置声明,在将模板的一个特定实例声明为友元时要用到
template <typename T> class Pal;
class C { // C 是普通类
friend class Pal<C>; // 用类 C 实例化的 Pal 是 C 的一个友元
// Pal2 的所有实例都是 C 的友元;这种无需前置声明
template <typename T> friend class Pal2;
};
template <typename T> class C2 { // C2 本身是一个类模板
// C2 的每个实例将相同实例化的 Pal 声明为友元
friend class Pal<T>;
// Pal2 的所有实例都是 C2 每个实例的友元,不需要前置声明
template <typename X> friend class Pal2;
// Pal3 是一个非模板类,它是 C2 所有实例的友元
friend class Pal3; // 不需要 Pal3 的前置声明
};
在新标准中,我们可以将模板类型参数声明为友元:
template <typename Type> class Bar {
friend Type; // 将访问权限授予用来实例化 Bar 的类型
// ...
};
此处我们将用来实例化 Bar 的类型声明为友元。即 Sales_data 将会成为 Bar
值得注意的是,虽然友元通常来说是一个类或函数,但我们完全可以用内置类型来实例化 Bar,这种与内置类型的友好关系是允许的。
类模板的一个实例定义了一个类类型,与任何其他类类型一样,我们可以定义一个 typedef 来引用实例化的类:
typedef Blob<string> StrBlob;
StrBlob x; // x 为 Blob
由于模板不是一个类型,我们不能定义一个 typedef 引用一个模板。即,无法定义一个 typedef 引用 Blob
template<typename T> using twin = pair<T,T>;
twin<string> authors; // authors 是 pair
一个模板类型别名就是一族类的别名:
twin<int> win_loss; // win_loss 是 pair
twin<double> area; // area 是 pair
当我们定义一个模板类型别名时,可以固定一个或多个模板参数:
template<typename T> using partNo = pair<T,unsigned>;
partNo<string> books; // books 是 pair
partNo<double> s; // s 是 pair
类模板可以声明 static 成员:
template<typename T> class Foo {
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
};
Foo 是一个类模板,它有一个名为 count 的 public static 成员函数和一个名为 ctr 的 private static 数据成员。每个 Foo 的实例都有其自己的 static 成员实例。即,对任意的给定类型 X,都有一个 Foo
Foo<int> fi,fi2,fi3; // 这三个对象共享相同的 Foo::ctr 和 Foo::count
与任何其他 static 数据成员相同,模板类的每个 static 数据成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的 static 对象,所以我们将 static 数据成员也定义为模版:
template<typename T>
size_t Foo<T>::ctr = 0; // 定义并初始化 ctr
这样,当使用一个特定的模板实参实例化 Foo 时,将会为该类类型实例化一个独立的 ctr,并将其赋值为 0。
与非模板类的静态成员类似,我们可以通过类类型对象来访问一个类模板的 static 成员,也可以使用作用域运算符直接访问成员。当然,为了通过类来直接访问 static 成员,我们必须引用一个特定的实例:
Foo<int> fi; // 实例化 Foo 类和 static 数据成员 ctr
auto ct = Foo<int>::count(); // 实例化 Foo::count
ct = fi.count(); // 使用 Foo::count
ct = Foo::count(); // 错误,使用哪个模板实例的 count
类似其他成员函数,一个 static 成员只有在使用时才会实例化。