类模板
类模板是用来生成类的蓝图的。
与函数模板的不同之处是,编译器不能为类模板推断模极参数类型。
如我们已经多次看到的,为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
定义类模板
类模板的格式如下所示:
template <typename T>
class ClassName {
// 类成员和函数声明与定义
};
其中,template <typename T>
表示该类是一个模板类,T
是一个占位符类型,可以在类内部使用。ClassName
是模板类的名称。
我们举个例子
#include <iostream>
// 类模板定义
template <typename T>
class MyClass {
public:
MyClass(T value) : value_(value) {}
void setValue(T value) {
value_ = value;
}
T getValue() const {
return value_;
}
void printValue() const {
std::cout << "Value: " << value_ << std::endl;
}
private:
T value_;
};
int main() {
// 使用int类型的类模板实例化对象
MyClass<int> intObject(42);
intObject.printValue(); // 输出: Value: 42
// 修改整数值并打印
intObject.setValue(100);
intObject.printValue(); // 输出: Value: 100
// 使用std::string类型的类模板实例化对象
MyClass<std::string> stringObject("Hello, Templates!");
stringObject.printValue(); // 输出: Value: Hello, Templates!
// 修改字符串值并打印
stringObject.setValue("New String Value");
stringObject.printValue(); // 输出: Value: New String Value
return 0;
}
在这个例子中,我们定义了一个简单的类模板MyClass
,它接受一个类型参数T
。类内部有一个私有成员变量value_
,其类型为T
。类中提供了构造函数来初始化value_
,以及setValue
和getValue
成员函数来修改和获取value_
的值。此外,还有一个printValue
成员函数来打印value_
的值。
在main
函数中,我们使用int
和std::string
两种类型分别实例化了MyClass
,展示了如何使用类模板来创建和操作不同类型的对象。通过这种方式,我们可以很容易地为多种数据类型重用相同的类定义,提高了代码的灵活性和可重用性。
实例化类模板
当我们想要创建一个特定类型的MyClass
对象时,我们需要提供一个具体的类型来替换模板参数T
。这个过程就是实例化。
例如,如果我们想要一个整数类型的MyClass
对象,我们可以这样实例化:
MyClass<int> intObject(42);
在这里,int
就是我们提供的具体类型,它替换了类模板中的T
。编译器会生成一个新的类,这个类是为int
类型专门定制的MyClass
。
MyClass<std::string> stringObject("Hello, Templates!");
此时,std::string
替换了模板中的T
,编译器会生成另一个专门为std::string
类型定制的MyClass
。
编译器的工作:
在编译时,当编译器遇到类模板的实例化请求,它会做以下几件事情:
- 替换模板中的所有
T
为提供的实际类型(如int
或std::string
)。 - 生成一个新的类定义,这个定义是专门为提供的类型创建的。
- 编译这个新生成的类定义,就像它是一个普通的类一样。
- 为新类型生成相应的成员函数、构造函数、析构函数等。
生成的对象:
一旦类模板被实例化,我们就可以像使用普通类一样使用这个新生成的类。例如,我们可以调用其成员函数,访问其成员变量等。
总之,实例化就是将泛型转变为具体类型,生成对应版本代码的过程
在模板作用域中引用模板类型
为了阅读模板类代码,应该记住类模板的名字不是一个类型名。
类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
可能令人迷惑的是,一个类模板中的代码如果使用了另外一个模板,通常不将一个实际类型(或值)的名字用作其模板实参。
相反的,我们通常将模板自己的参数当作被使用模板的实参。
这些描述涉及到了模板的嵌套使用,也就是说,在一个模板类内部使用了另一个模板,并且使用当前模板的参数作为内部模板的参数。
下面是一个例子来说明这个概念:
#include <vector>
#include <iostream>
// 定义一个模板类A,它有一个模板参数T
template <typename T>
class A {
public:
std::vector<T> vec; // 使用模板参数T实例化std::vector
void printVec() {
for (const auto& elem : vec) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
};
// 定义一个模板类B,它有两个模板参数:一个是类型参数U,另一个是类模板参数A<T>
template <typename U, typename T>
class B {
public:
A<U> a_obj; // 使用外部模板参数U实例化类模板A
void addElement(U elem) {
a_obj.vec.push_back(elem);
}
void printVecB() {
a_obj.printVec();
}
};
int main() {
// 实例化模板类B,其中U为int,T也为int(用于实例化A<int>)
B<int, int> b_obj;
b_obj.addElement(10);
b_obj.addElement(20);
b_obj.addElement(30);
b_obj.printVecB(); // 输出:10 20 30
return 0;
}
在这个例子中,A<T> 是一个模板类,它有一个 std::vector<T> 成员。B<U, T> 是另一个模板类,它有一个 A<U> 类型的成员。在 B<U, T> 类模板中,我们使用了外部模板参数 U 来实例化 A<T>,这就是“将模板自己的参数当作被使用模板的实参”的一个例子。
在 main 函数中,我们实例化了 B<int, int>,这意味着我们创建了一个包含 A<int> 成员的 B 类实例。这就是类模板实例化的一个具体例子,其中模板参数被实际类型(在这个例子中是 int)所替代。
类模板的成员函数
与其他任何类相同,我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。
因此,类模板的成员函数具有和模板相同的模板参数。
因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。
与往常一样,当我们在类外定义一个成员时,必须说明成员属于哪个类。而且,从一个模板生成的类的名字中必须包含其模板实参。当我们定义一个成员函数时,模板实参与模板形参相同。
// 声明类模板
template<typename T>
class MyClass {
public:
MyClass(T value);
void printValue();
private:
T value_;
};
// 在类外定义构造函数
template<typename T>
MyClass<T>::MyClass(T value) : value_(value) {
// 构造函数的实现
}
// 在类外定义成员函数
template<typename T>
void MyClass<T>::printValue() {
std::cout << "Value: " << value_ << std::endl;
}
int main() {
MyClass<int> obj(42);
obj.printValue(); // 输出:Value: 42
return 0;
}
在这个例子中,MyClass 是一个类模板,它有一个类型参数 T。
构造函数 MyClass(T value) 和成员函数 void printValue() 都在类声明之外进行了定义。
注意,在类外定义模板类的成员函数时,必须使用 template<typename T> 开头,并且函数名前面要加上 MyClass<T>:: 来指明这个函数是哪个模板类的成员。
这种方式的好处是可以让类声明更加简洁,而将函数的复杂实现细节隐藏在类声明之外。同时,如果函数实现非常长或者需要包含其他头文件,这样做也可以减少类声明部分的依赖和复杂性。
类模板成员函数的实例化
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。
在C++中,类模板的成员(成员函数或静态数据成员)并不是在类模板实例化时就被全部实例化。相反,成员函数只在实际被调用时才被实例化,而静态数据成员则在其定义时被实例化。
这种行为有助于减少编译时间和生成的代码大小,因为只有当特定的成员函数真正需要时,编译器才会为其生成代码。
下面是一个例子,演示了类模板成员函数在实际使用时才被实例化的概念:
#include <iostream>
// 类模板声明
template<typename T>
class MyClass {
public:
MyClass(T val) : value(val) {}
// 成员函数声明
void displayValue();
void unusedFunction();
private:
T value;
};
// 成员函数定义
template<typename T>
void MyClass<T>::displayValue() {
std::cout << "Value: " << value << std::endl;
}
// 另一个成员函数定义,但这个函数在后面的代码中从未被调用
template<typename T>
void MyClass<T>::unusedFunction() {
std::cout << "This function is not used." << std::endl;
}
int main() {
// 使用int类型实例化类模板
MyClass<int> intObject(42);
intObject.displayValue(); // 此处调用了displayValue,因此该函数会被实例化
// 注意:unusedFunction没有被调用,因此在这次编译中它不会被实例化
return 0;
}
在上面的代码中,MyClass是一个类模板,它有两个成员函数:displayValue和unusedFunction。在main函数中,我们创建了一个MyClass<int>的实例,并且只调用了displayValue函数。因此,只有displayValue函数在这次编译过程中被实例化。尽管unusedFunction也是MyClass的成员函数,但由于在main函数中从未被调用,所以它在这次编译中不会被实例化。
编译器优化通常会确保只有真正需要的代码才会被生成,这有助于减少最终可执行文件的大小并提高性能。如果你尝试在代码中查找unusedFunction函数的实例化,你会发现它并不存在,除非你在某处显式地调用了它。
如果一个成员函数没有被使用,则它不会被实例化。
成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
在类代码内简化模板类名的使用
当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。
在类模板自己的作用域中,我们可以直接使用模板名而不提供实参:
在类模板的定义内部,当引用模板自身或其成员时,我们不需要提供模板实参。编译器能够推断出当前的上下文,并知道我们是在引用模板自身的当前实例化。这意味着,在类模板成员函数或内部类型的定义中,我们可以直接使用模板名来引用当前模板实例的成员。
例如,考虑以下类模板定义:
template <typename T>
class MyClass {
public:
MyClass() : value(T()) {}
void setValue(T newValue) {
value = newValue;
}
T getValue() const {
return value;
}
// 在成员函数内部,我们可以直接使用MyClass而不提供模板实参
MyClass* createCopy() const {
return new MyClass(*this); // 这里MyClass指的是MyClass<T>
}
private:
T value;
};
在成员函数createCopy中,我们使用了new MyClass(*this);来创建一个当前模板实例的拷贝。在这个上下文中,MyClass自动解析为MyClass<T>,其中T是模板实例化的类型。我们不需要写成new MyClass<T>(*this);,因为编译器已经知道我们在引用哪个实例化。
同样地,如果我们在类模板内部定义一个嵌套类型或函数,我们也可以直接使用模板名:
template <typename T>
class Outer {
public:
class Inner { // 嵌套类定义
public:
Inner() : value(T()) {}
T value;
};
// 成员函数内部使用嵌套类
Inner createInner() {
return Inner(); // 这里Inner自动指的是Outer<T>::Inner
}
};
在这个例子中,Inner类是在Outer类模板内部定义的,当我们在Outer的方法中引用Inner时,也不需要提供模板实参。编译器理解我们在引用当前Outer<T>实例中的Inner类。
在类模板外使用类模板名
当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,直到遇到类名才表示进入类的作用域:
//后置:递增/递减对象但返回原值
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
//此处无须检查;调用前置递增时会进行检查
BlobPtr ret = *this; //保存当前值
++*this; //推进一个元素;前置++检查递增是否合法
return ret;// 返回保存的状态
}
由于返回类型位于类的作用域之外,我们必须指出返回类型是一个实例化的 BlobPtr,它所用类型与类实例化所用类型一致。
在函数体内,我们已经进入类的作用域,因此在定义 ret时无须重复模板实参。如果不提供模板实参,则编译器将假定我们使用的类型与成员实例化所用类型一致。
因此,ret的定义与如下代码等价:
BlobPtr<T> ret =*this;
在一个类模板的作用城内,我们可以直接使用模板名而不必指定模板实参。
类模板和友元
当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。
如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
类模板与另一个(类或函数)模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。
以下是一个结合了类模板和函数模板的例子,
其中类模板MyClass表示一个简单的动态数组,而函数模板PrintArray则被设计为能够打印任意类型的数组。PrintArray函数模板被声明为MyClass的友元,因此它可以访问MyClass的私有成员。
#include <iostream>
#include <vector>
// 类模板 MyClass,表示一个简单的动态数组
template<typename T>
class MyClass {
// 声明 PrintArray<T> 为 MyClass<T> 的友元
template<typename U> friend void PrintArray(const MyClass<U>& obj);
private:
std::vector<T> data;
public:
MyClass(std::initializer_list<T> list) : data(list) {}
void add(const T& value) {
data.push_back(value);
}
};
// 函数模板 PrintArray,用于打印 MyClass 类型的数组内容
template<typename T>
void PrintArray(const MyClass<T>& obj) {
std::cout << "[";
for (size_t i = 0; i < obj.data.size(); ++i) {
std::cout << obj.data[i];
if (i < obj.data.size() - 1) {
std::cout << ", ";
}
}
std::cout << "]" << std::endl;
}
int main() {
MyClass<int> intArray = {1, 2, 3, 4, 5};
MyClass<double> doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
PrintArray(intArray); // 输出: [1, 2, 3, 4, 5]
PrintArray(doubleArray); // 输出: [1.1, 2.2, 3.3, 4.4, 5.5]
return 0;
}
在这个例子中,MyClass是一个类模板,它使用std::vector来存储数据,并提供了一个添加元素的方法。PrintArray是一个函数模板,它被设计为可以打印任何类型的MyClass对象的内容。由于PrintArray被声明为MyClass的友元函数模板,因此它可以访问MyClass的私有成员data。
在main函数中,我们创建了两个MyClass对象,一个用于整数,另一个用于双精度浮点数,并使用PrintArray函数模板来打印它们的内容。由于PrintArray是模板函数,它可以与任何类型的MyClass一起使用,只要这些类型是兼容的。
通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元:
//前置声明,在将模板的一个特定实例声明为友元时要用到
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>;// Pal的模板声明必须在作用域之内
// Pal2的所有实例都是C2的每个实例的友元,不需要前置声明
template <typename X> friend class Pal2;
// Pal3是一个非模板类,它是C2所有实例的友元
friend class Pal3;// 不需要Pa13的前置声明
};
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
令模板自己的类型参数成为友元
在新标准中,我们可以将模板类型参数声明为友元;
template <typename Type>
class Bar
{
friend Type;// 将访问权限授予用来实例化Bar的类型
//...
};
此处我们将用来实例化Bar的类型声明为友元。
因此,对于某个类型名Foo,Foo将成为为Bar<Foo>的友元,Sales_data将成为Bar<Sales_data>的友元,依此类推,
值得注意的是,虽然友元通常来说应该是一个类或是一个函数,但我们完全可以用一个内置类型来实例化Bar。
这种与内置类型的友好关系是允许的,以便我们能用内置类型来实例化Bar这样的类。
模板类型别名
类模板的一个实例定义了一个类类型,与任何其他类类型一样,我们可以定义一个typedef来引用实例化的类:
typedef Blob<string> StrBlob;
由于模板不是一个类型,我们不能定义一个typedef引用一个模板。即,无法定义一个typedef引用Blob<T>
但是,新标准允许我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors是一个pair<string, string>
在这段代码中,我们将twin定义为成员类型相同的pair的别名。这样,twin的用户只需指定一次类型。
一个模板类型别名是一族类的别名:
twin<int> win_loss; // win_loss 是一个pair<int, int>
twin<double> area; // area是一个pair<double, double>
就像使用类模板一样,当我们使用twin时,需要指出希望使用哪种特定类型的twin。
当我们定义一个模板类型别名时,可以固定一个或多个模板参数;
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books;// books是一个pair<string, unsigned>
partNo<Vehicle> cars;// cars是一个pair<Vehicle, unsigned>
partNo<Student> kids; // kids是一个pair<student, unsigned>
这段代码中我们将 partNo 定义为一族类型的别名,这族类型是 second 成员为unsigned的pair. partNo的用户需要指出pair的first成员的类型,但不能指定second成员的类型。
类模板的static成员
与任何其他类相同,类模板可以声明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<x>::ctr 和一个 Foo<x>::count成员。所有Foo<X>类型的对象共享相同的ctr对象和count函数。
例如,
//实例化static 成员 Foo<string>::ctr和Foo<string>::count
Foo<string> fs;
//所有三个对象共享相同的Foo<int>::ctr和Foo<int>::count成员
Foo<int> fi, fi2, fi3;
与任何其他static数据成员相同,模板类的每个static数据成员必须有且仅有一个定义。
但是,类模板的每个实例都有一个独有的static对象。
因此,与定义模板的成员函数类似,我们将static数据成员也定义为模板:
template <typename T>
size_t Foo<T>::ctr =0;//定义并初始化ctr
与类模板的其他任何成员类似,定义的开始部分是模板参数列表,随后是我们定义的成员的类型和名字。与往常一样,成员名包括成员的类名,对于从模板生成的类来说,类名包括模板实参。因此,当使用一个特定的模板实参类型实例化Foo时,将会为该类类型实例化一个独立的ctr,并将其初始化为0。
与非模板类的静态成员相同,我们可以通过类类型对象来访问一个类模板的static成员,也可以使用作用域运算符直接访问成员。
当然,为了通过类来直接访问static成员,我们必须引用一个特定的实例:
Foo<int> fi; // 实例化Foo<int>类和static数据成员ctr
auto ct= Foo<int>::count(); //实例化Foo<int>::count
//使用 Foo<int>::count
ct = fi.count();
ct = Foo::count (); // 错误:使用哪个模板实例的count?
类似任何其他成员函数,一个static成员函数只有在使用时才会实例化。