模板参数
什么是模板参数
模板参数是在C++中使用模板时,用于指定模板的参数的一种机制。模板参数可以是类型参数、非类型参数或模板参数。
- 类型参数是指在模板中使用的特定类型,可以是内置类型(如int、float等)、自定义的类类型或其他模板类型。
- 非类型参数是指在模板中使用的常量值,可以是整数、字符、枚举或指针。
模板参数可以用于定义模板函数、模板类或模板别名。通过在模板定义中指定参数的类型或值,可以根据实际需求生成特定的函数或类。
例如,以下是一个模板函数的示例,其中T是类型参数,n是非类型参数:
template <typename T, int n>
void printArray(T arr[n]) {for (int i = 0; i < n; i++) {cout << arr[i] << " ";}cout << endl;
}
在使用该模板函数时,需要指定类型参数T和非类型参数n,例如:
int main() {int arr[] = {1, 2, 3, 4, 5};printArray<int, 5>(arr);return 0;
}
在上述示例中,类型参数T被指定为int,非类型参数n被指定为5,从而生成一个可以打印整型数组的函数。
类似函数参数的名字,一个模板参数的名字也没有什么内在含义。
我们通常将类型参数命名为T,但实际上我们可以使用任何名字:
template <typename Foo> Foo calc(const Foo& a, const Foo& b)
{
Foo tmp = a; // tmp的类型与参数和返回类型一样
//...
return tmp: //返回类型和参数类型一样
}
模板参数与作用域
模板参数遵循普通的作用域规则。
一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。
与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。
但是,与大多数其他上下文不同,在模板内不能重用模板参数名:
typedef double A;
template <typename A, typename B> void f (A a, B b)
{
A tmp = a; // tmp的类型为模板参数A的类型,而非double
double B; // 错误:重声明模板参数 B
}
正常的名字隐藏规则决定了A的typedef 被类型参数A 隐藏。因此,tmp不是一个double,其类型是使用f时绑定到类型参数A的类型。
由于我们不能重用模板参数名,声明名字为B的变量是错误的。
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次:
//错误:非法重用模板参数名V
template <typename V, typename V> //
模板声明
模板声明必须包含模板参数:
// 声明但不定义 compare和Blob
<typename T>int compare(const T&, const T&);
template <typename T> class Blob;
声明中的模板参数的名字不必与定义中相同:
// 3个calc都指向相同的函数模板
template <typename T>T calc(const T&, const T&);// 声明
template <typename U> U calc(const U&, const U&); //声明// 模板的定义
template <typename Type>
Type calc(const Type& a, const Type& b) { /*... */}
当然,一个给定模板的每个声明和定义必须有相同数量和种类(即,类型或非类型)的参数。
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于Best
任何使用这些模板的代码之前
使用类的类型成员
回忆一下,我们用作用域运算符(::)来访问static成员和类型成员。
在普通(非模板)代码中,编译器掌握类的定义。因此,它知道通过作用域运算符访问的名字是类型还是static成员。
例如,如果我们写下string::size_type,编译器有string的定义,从而知道size type是一个类型。
但对于模板代码就存在困难。
例如,假定工是一个模板类型参数,当编译器遇到类似T::mem这样的代码时,它不会知道mem是一个类型成员还是一个static数据成员,直至实例化时才会知道。
但是,为了处理模板,编译器必须知道名字是否表示一个类型。
例如,假定T是一个类型参数的名字,当编译器遇到如下形式的语句时:
T::size_type * p;
它需要知道我们是正在定义一个名为p的变量还是将一个名为size_type的static数据成员与名为p的变量相乘。
默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。
因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。
我们通过使用关键字typename来实现这一点:
template <typename T>
typename T::value type top(const T& c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}
我们的top函数期待一个容器类型的实参,它使用typename指明其返回类型并在c中没有元素时生成一个值初始化的元素返回给调用者。
当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。
默认模板实参
就像我们能为函数参数提供默认实参一样,我们也可以提供默认模板实参。
在新标准中,我们可以为函数和类模板提供默认实参。而更早的C++标准只允许为类模板提供默认实参。
例如,我们重写compare,默认使用标准库的less函数对象模板:
// compare有一个默认模板实参1ess<T>和一个默认函数实参F()
template <typename T, typename F=less<T>>
int compare(const T &vl, const T &v2, F f=F())
{
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
在这段代码中,我们为模板添加了第二个类型参数,名为F,表示可调用对象的类型;并定义了一个新的函数参数f,绑定到一个可调用对象上。
我们为此模板参数提供了默认实参,并为其对应的函数参数也提供了默认实参。
默认模板实参指出 compare 将使用标准库的 less函数对象类,它是使用与compare一样的类型参数实例化的。默认函数实参指出f将是类型F的一个默认初始化的对象。
当用户调用这个版本的compare时,可以提供自己的比较操作,但这并不是必需的:
bool i = compare(0,42);//使用less;i为-1
// 结果依赖于iteml 和item2 中的isbn
Sales_data iteml (cin), item2 (cin);
bool j = compare(iteml, 1tem2, compareIsbn);
第一个调用使用默认函数实参,即,类型less<T>的一个默认初始化对象。
在此调用中,T为int,因此可调用对象的类型为less<int>。compare的这个实例化版本将使用less<int>进行比较操作。
在第二个调用中,我们传递给compare三个实参:compareIsbn 和两个Sales_data类型的对象。当传递给compare三个实参时,第三个实参的类型必须是一个可调用对象,该可调用对象的返回类型必须能转换为bool值,且接受的实参类型必须与compare的前两个实参的类型兼容。
与往常一样,模板参数的类型从它们对应的函数实参推断而来。在此调用中,T的类型被推断为Sales data,F被推断为compareIsbn的类型。
与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
模板默认实参与类模板
无论何时使用一个类模板,我们都必须在模板名之后接上尖括号。
尖括号指出类必须从一个模板实例化而来特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:
template <class T = int> class Numbers { // T默认为int
public:
Numbers(T v = 0): val(v) ()
// 对数值的各种操作
private:
T val;
Numbers<long double> lots of_precision;
Numbers<> average_precision; // 空<>表示我们希望使用默认类型
此例中我们实例化了两个Numbers版本:average_precision是用int代替T实例化得到的;lots of precision是用long double代替T实例化而得到的。
成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板(member template)。成员模板不能是虚函数。
普通(非模板)类的成员模板
我们直接看例子
下面是一个简单的例子,其中定义了一个名为MyClass的类,它有一个成员函数模板printValue(),可以打印任何类型的值:
#include <iostream>class MyClass {
public:// 成员函数模板定义template <typename T>void printValue(T value) {std::cout << "Value: " << value << std::endl;}
};int main() {MyClass obj;obj.printValue(5); // 输出:Value: 5obj.printValue(3.14); // 输出:Value: 3.14obj.printValue("Hello"); // 输出:Value: Helloreturn 0;
}
在这个例子中,printValue()函数可以接受任何类型T的参数,并将其打印出来。
在C++中,成员函数模板可以在类内定义,也可以在类外定义。下面是这两种定义方式的详细解释:
类内定义
在类内部直接定义成员函数模板是非常方便和直观的。这种方式通常用于较短的函数实现,以便将函数的声明和实现紧密地结合在一起。
class MyClass {
public:// 成员函数模板的类内定义template <typename T>void printValue(T value) {std::cout << "Value: " << value << std::endl;}
};
在类内定义时,模板参数和函数体都直接写在类定义中。这种方式适用于函数体较短、逻辑简单的情况。
类外定义
如果成员函数模板的实现比较复杂,或者为了保持类定义的简洁性,你可能希望在类外部定义成员函数模板。在类外定义时,你需要使用作用域解析运算符(::)来指明这个函数属于哪个类。
class MyClass {
public:// 成员函数模板的声明template <typename T>void printValue(T value);
};// 成员函数模板的类外定义
template <typename T>
void MyClass::printValue(T value) {std::cout << "Value: " << value << std::endl;
}
在类外定义时,你首先在类内部声明成员函数模板,然后在类外部提供具体的实现。注意,在类外定义成员函数模板时,template 关键字和模板参数列表仍然需要放在函数实现之前,并且要使用类名和作用域解析运算符来指明这是哪个类的成员函数。
注意事项
- 无论是类内定义还是类外定义,成员函数模板都是在编译时展开的,针对每种不同的类型,编译器都会生成相应的函数实例。
- 当你在类外定义成员函数模板时,需要确保模板的定义在使用它的每个编译单元中都是可见的。这通常意味着你需要将成员函数模板的定义放在头文件中,以便多个源文件可以包含它。
- 如果成员函数模板只在类内部使用,并且实现较短,类内定义是更简洁的选择。如果函数实现较长或者逻辑复杂,类外定义可能更为合适。
类模板的成员模板
类模板的成员模板是指在类模板内部定义的成员函数模板或成员类模板。这种结构提供了两层的模板化:外层是类模板,它允许你以类型参数化的方式定义类;内层是成员模板,它允许类的成员函数或内部类也接受类型参数。
类模板的成员函数模板
当你在一个类模板中定义一个成员函数模板时,你实际上是在为一个已经模板化的类添加了一个可以接受额外类型参数的函数。下面是一个简单的例子:
#include <iostream>
#include <vector>// 类模板定义
template<typename C>
class MyClass {
public:// 成员函数模板声明template<typename T>void process(const std::vector<T>& data) {for (const auto& item : data) {std::cout << "Processing " << item << std::endl;}}// 普通的成员函数,用于对比void process(const std::vector<C>& data) {for (const auto& item : data) {std::cout << "Processing class template type " << item << std::endl;}}
};int main() {MyClass<int> myIntClass;std::vector<int> ints = {1, 2, 3, 4, 5};std::vector<double> doubles = {1.1, 2.2, 3.3, 4.4, 5.5};// 调用成员函数模板myIntClass.process(doubles); // 使用成员函数模板处理double类型的vector// 调用普通的成员函数myIntClass.process(ints); // 使用普通的成员函数处理int类型的vectorreturn 0;
}
在这个例子中,MyClass 是一个类模板,它有一个类型参数 C。类内部定义了一个成员函数模板 process,它接受一个类型为 std::vector<T> 的参数。这意味着你可以为 process 函数传递任何类型的 std::vector,而不仅限于类模板参数 C 指定的类型。
类模板的成员类模板
除了成员函数模板,你还可以在类模板内部定义成员类模板。这种情况比较少见,但在某些复杂的设计中可能是有用的。
template<typename Outer>
class OuterClass {
public:template<typename Inner>class InnerClass {public:Inner value;InnerClass(Inner val) : value(val) {}void print() const {std::cout << "Inner value: " << value << std::endl;}};
};int main() {OuterClass<int>::InnerClass<double> inner(3.14);inner.print(); // 输出:Inner value: 3.14return 0;
}
在这个例子中,OuterClass 是一个类模板,它有一个成员类模板 InnerClass。InnerClass 也是一个模板,可以独立于 OuterClass 的模板参数进行实例化。
总的来说,类模板的成员模板提供了极高的灵活性,允许你在不同类型的上下文中以类型安全的方式重用代码。然而,它们也增加了代码的复杂性,因此需要谨慎使用。
类模板的成员模板也可以在类内定义或在类外定义。下面是这两种定义方式的详细解释:
类内定义
在类模板内部直接定义成员模板是非常方便的。这种方式通常用于较短的函数或类实现,以保持代码的紧凑性和可读性。
成员函数模板的类内定义
template<typename TClass>
class MyClass {
public:// 成员函数模板的类内定义template<typename T>void printValue(T value) {std::cout << "Value of type " << typeid(T).name() << ": " << value << std::endl;}
};
在这个例子中,printValue 是一个成员函数模板,它在类模板 MyClass 内部定义。这种方式使得函数的实现与声明紧密结合,便于阅读和维护。
成员类模板的类内定义
template<typename TOuter>
class OuterClass {
public:// 成员类模板的类内定义template<typename TInner>class InnerClass {public:TInner value;InnerClass(TInner val) : value(val) {}void print() {std::cout << "Inner value: " << value << std::endl;}};
};
这里,InnerClass 是在 OuterClass 类模板内部定义的成员类模板。
类外定义
如果成员模板的实现较为复杂,或者为了保持类定义的简洁性,可以将其定义在类外部。
与类模板的普通函数成员不同,成员模板是函数模板。当我们在类模板外定义一个成674 员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后
跟成员自己的模板参数列表:
成员函数模板的类外定义
template<typename TClass>
class MyClass {
public:// 成员函数模板的声明template<typename T>void printValue(T value);
};// 成员函数模板的类外定义
template<typename TClass>
template<typename T>
void MyClass<TClass>::printValue(T value) {std::cout << "Value of type " << typeid(T).name() << ": " << value << std::endl;
}
在这个例子中,printValue 成员函数模板首先在类模板 MyClass 内部声明,然后在类外部进行了定义。注意类外定义时的语法:需要两层 template 关键字,第一层用于类模板参数,第二层用于成员函数模板参数。
成员类模板通常不采用类外定义,因为它们往往与外围类紧密相关,并且它们的定义不太可能非常长或复杂到需要移出类定义之外。然而,如果需要,理论上也是可以在类外定义的,但这样做会使代码变得难以阅读和维护。
注意事项
- 无论是类内定义还是类外定义,成员模板都是在编译时展开的,编译器会为每种不同的类型生成相应的实例。
- 类外定义成员模板时,需要确保模板的定义在使用它的每个编译单元中都是可见的,这通常意味着将定义放在头文件中。
- 类模板的成员模板增加了代码的复杂性,应谨慎使用,并确保其带来的灵活性确实是项目所需的。
控制实例化
当模板被使用时才会进行实例化这一特性意味着,相同他实例可能出现在多个对象文件中。
当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化来避免这种开销。
一个显式实例化有如下形式:
extern template declaration; // 实例化声明
template declaration; // 实例化定义
declaration是一个类或函数声明,其中所有模板参数已被管换为模板实参。例如./7实例化声明与定义
extern template class Blob<string>; // 声明
template int compare(conat int&, const int&);// 定义
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。
将一个实例化声明为extern 就表示承诺在程序其他位置有该实例化的一个非extern于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前:
// Application.cc
//这些模板类型必须在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sal,sa2;// 实例化会出现在其他位置
// Blob<int>及其接受initializer list的构造函数在本文件中实例化
Blob<int> al = {0,1,2,3,4,5,6,7,8,9};
Blob<int> a2(a1);//拷贝构造函数在本文件中实例化
int i = compare(al[0],a2[0]);//实例化出现在其他位置
文件Application.o将包含Blob<int>的实例及其接受initializer_list参数的构造函数和拷贝构造函数的实例。
而compare<int>函数和Blob<string>类将不在本文件中进行实例化。这些模板的定义必须出现在程序的其他文件中:
// templateBuild.cc
// 实例化文件必须为每个在其他文件中声明为extern的类型和函数提供一个(非extern)
//的定义
template int compare(const int, const int&);
template class Blob<string>;// 实例化类模板的所有成员
当编译器遇到一个实例化定义(与声明相对)时,它为其生成代码。
因此,文件templateBuild.o将会包含compare的int实例化版本的定义和Blob<string>类的定义。当我们编译此应用程序时,必须将templateBuild.o和Application.o链接到一起。
对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。
实例化定义会实例化所有成员
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。
当编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数。
因此,与处理类模板的普通实例化不同,编译器会实例化该类的所有成员。即使我们不使用某个成员,它也会被实例化。
因此,我们用来显式实例化一个类模板的类型, 必须能用于模板的所有成员。