16.1.2 类模板
类模板(class template)可以用来生成类的蓝图。与函数模板的不同之处在于,编译器不能为类模板推断模板参数类型。
为了使用类模板,必须在模板名后的尖括号中提供额外的信息——用来代替模板参数的模板实参列表。
定义类模板
作为一个例子,我们将实现 StrBlob 类的模板版本。将它重新命名为 Blob,不再针对 string。
类似于函数模板,类模板也以关键字 template 开始,后跟模板参数列表。
template <typename T> class Blob {public:typedef T value_type;typedef typename std::vector<T>::size_type size_type;// 👆 typedef 方法的第一个参数是内置关键字, 第二个参数是自定义关键字// 👇 构造函数Blob();Blob(std::initializer_list<T> il);// 👇 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:// 使用 shared_ptr 指向 datastd::shared_ptr<std::vector<T>> data;// 👇 若 data[i] 无效, 则抛出 msgvoid check(size_type i, const std::string &msg) const;
};
Blob 模板有一个名为 T 的模板参数类型,用来表示 Blob 保存的元素的类型。
实例化类模板
根据之前的学习,我们已经多次见到,到使用一个类模板时,必须提供额外的信息,最典型最常用的一个就是声明一个保存某种类型数据的 vector 容器。现在我们知道这些额外信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化特定的类。
例如:
Blob<int> ia;Blob<int> ia2 = {0, 1, 2, 3, 4};
当编译器从我们的 Blob 模板实例化出一个类时,它会重写 Blob 模板,将模板参数 T 的每个实例替换为给定的模板实参,本例为 int。
一个类模板的每个实例都形成一个独立的类。类型 Blob<string>
与任何其它 Blob 类型都没有关联,也不会对任何其它 Blob 类型的成员有特殊访问权限。
在模板作用域中引用模板类型
类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
类模板的成员函数
与任何其它类型相同,我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式地声明为内联函数。
类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数必须以关键字 template 开始,后接类模板参数列表。
与往常一样,当我们在类外定义一个成员时,必须说明成员属于哪个类。且,从一个模板生成的类的名字中必须包含模板实参。当我们定义一个成员函数时,模板实参与模板形参相同:
template<typename T> ret-type Blob<T>::member_name(parm-list)
check 和元素访问成员
我们首先定义 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);
}
除了类名中的不同之处以及使用了模板参数列表外,此函数与原 StrBlob 类的 check 成员完全一样。
下标运算符和 back 函数用模板参数指出返回类型,其它未变:
template <typename T>
T& Blob<T>::back() {check(0, "back on empty Blob");return data -> back();
}template <typename T>
T& Blob<T>::operator[](size_type i) {check(i, "subscript out of range");return (*data)[i];
}template <typename T>
void Blob<T>::pop_back() {check(0, "pop_back on empty Blob");data -> pop_back();
}
Blob 构造函数
与其它任何定义在类模板外的成员一样,构造函数的定义要以模板参数开始:
template <typename T>
Blob<T>::Blob(): data(std::make_shared<std::vector<T>>) { }
这段代码在作用域 Blob<T>
中定义了名为 Blob 的成员函数。
类似的,接受一个 initializaer_list 参数的构造函数将其类型参数 T 作为 initializaer_list 参数的元素类型:
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il): data(std::make_shared<std::vector<T>>(il)) { }
为了使用这个构造函数,我们必须传递给它一个 initializer_list,其中的元素必须与 Blob 中的元素类型兼容:
Blob<string> articles = {"a", "an", "the"};
构造函数中的参数类型为 initializer_list<string>
。列表中每个字符串字面常量隐式地转为一个 string。
类模板成员函数的实例化
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。即,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
在类代码内简化模板类名的使用
当我们使用一个类模板类型时,必须提供模板实参(即 typename T),但这一规则有一个例外:在类模板自己的作用域当中,可以直接使用模板名而不提供实参。
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--();private:std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const;std::weak_ptr<std::vector<T>> wptr;std::size_t curr;
};
可以看到,在operator++()
的声明当中,其返回值是BlobPtr&
而非BlobPtr<T>&
,当我们处于一个类模板的作用域时,编译器处理模板自身引用时就好像我们提供了与模板参数匹配的实参一样。
在类模板外使用类模板名
在类模板外定义其它成员时,必须要知道的一点是,此时我们并不处于类的作用域当中,直到遇到类名才表示进入了类的作用域:
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int) {BlobPtr ret = *this;++ *this;return ret;
}
可以看到在上述函数定义的第一行,我们已经进入了类的作用域,因此可以写成BlobPtr ret = *this;
,它等价于BlobPtr<T> ret = *this;
。
类模板和友元
当一个类含有一个友元声明时,类与友元各自是否是模板是相互无关的。
一对一友好关系
类模板与另一个(类或函数)模板间的友好关系的最常见的形式是建立对应实例及其友元间的友好关系。例如,Blob 类应该将 BlobPtr 类和一个运算对象是 Blob 类的相等运算符定义为友元:
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 {friend class BlobPtr<T>;friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
};
我们首先需要将 Blob、BlobPtr 和 operator== 声明为模板。这些声明是 operator== 函数的参数声明以及 Blob 中的友元声明所需要的。
通用和特定模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元。
令模板自己的类型参数为友元
在 C++ 11 标准当中,可以将模板类型参数声明为友元:
template <typename Type> class Bar {friend Type;// ... ... ...
};
对于 Bar<Foo>
,Foo 将成为 Bar<Foo>
的友元。
模板类型别名
由于模板不是一个类型,我们不能定义一个 typedef 引用一个模板,即:无法定义一个 typedef 引用 Blob<T>
。
C++ 11 标准允许我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors 是一个 pair<string string>
一个模板类型别名是一族类的别名:
twin<int> win_loss; // pair<int, int>
twin<double> area; // pair<double, double>
当我们定义一个模板类型别名时,可以固定一个或多个模板参数:
template<typename T> using partNo = pair<T, unsigned>;
partNo<string> books; // pair<string, unsigned>
类模板的 static 成员
与任何其它类相同,类模板可以声明 static 成员:
template<typename T> class Foo {public:static std::size_t count() { return ctr; }// ... ... ...private:static std::size_t ctr;// ... ... ...
};
Foo 的每个实例都有自己的 static 成员实例(而不是通用一个静态成员)。