模板是 C++ 的一个独特特性,通过它函数和类能够支持通用数据类型——换句话说,我们可以实现独立于特定数据类型的函数或类;例如,客户端可以请求max()
函数来处理不同的数据类型。我们可以只实现一个max()
并将数据类型作为参数传递,而不是使用函数重载来实现和维护许多类似的函数。此外,模板可以与多重继承和运算符重载一起工作,在 C++ 中创建强大的通用数据结构和算法,如标准模板库 ( STL )。此外,模板还可以应用于编译时计算、编译时和运行时代码优化等。
在本章中,我们将学习函数和类模板的语法、它们的实例化以及它们的专门化。然后,我们将介绍变量模板及其应用。接下来,我们将讨论模板参数以及用于实例化它们的相应参数。之后,我们将学习如何实现一种类型特征,以及如何使用这种类型的信息来优化算法。最后,我们将展示在程序执行时可以用来加速程序的技术,包括编译时计算、编译时代码优化和静态多态性。
本章将涵盖以下主题:
- 探索函数和类模板
- 理解可变模板
- 了解模板参数和参数
- 什么是特质?
- 模板元编程及其应用
本章的代码可以在本书的 GitHub 资源库中找到:https://github.com/PacktPublishing/Expert-CPP。
我们将从介绍函数模板的语法及其实例化、演绎和专门化开始这一部分。然后,我们将继续讨论类模板,看看类似的概念和例子。
到目前为止,当我们定义了一个函数或类时,我们必须提供输入、输出和中间参数。例如,假设我们有一个函数执行两个 int 类型整数的加法。我们如何扩展它,使其处理所有其他基本数据类型,如浮点、双精度、char 等?一种方法是通过手动复制、粘贴和稍微修改每个函数来使用函数重载。另一种方法是定义一个宏来执行加法操作。这两种方法都有各自的副作用。
此外,如果我们修复了一个 bug 或者为一种类型添加了一个新特性,并且这个更新需要在以后为所有其他重载函数和类完成,会发生什么呢?我们有没有更好的方法来处理这种情况,而不是使用这种愚蠢的复制粘贴替换方法?
事实上,这是任何计算机语言都可能面临的一个一般性问题。由通用函数式编程元语言 ( ML )于 1973 年首创,ML 允许编写公共函数或类型,这些函数或类型仅在使用时操作的类型集合上有所不同,从而减少了重复。后来受到特许人寿保险公司 ( CLU )提供的参数化模块和 Ada 提供的泛型的启发,C++ 采用了模板概念,允许函数和类使用泛型类型进行操作。换句话说,它允许函数或类处理不同的数据类型,而不需要重写它们。
实际上,从抽象的角度来看,C++ 函数或类模板(如 cookie cutters)充当了创建其他类似函数或类的模式。这背后的基本思想是创建一个函数或类模板,而不必指定某些或所有变量的确切类型。相反,我们使用占位符类型定义函数或类模板,称为模板类型参数。一旦我们有了函数或类模板,我们就可以通过使用已经在其他编译器中实现的算法来自动生成函数或类。
C++ 中有三种模板:函数模板、类模板、变量模板。我们接下来会看看这些。
函数模板定义了如何生成函数族。这里的族是指一组行为相似的函数。如下图所示,这包括两个阶段:
- 创建函数模板;也就是如何写的规则。
- 模板实例化;也就是说,用于从模板生成函数的规则:
Function template format
在上图的第一部分中,我们讨论了将用于创建泛型类型的函数模板的格式,但是关于专用模板,我们也称其为主模板。然后在第二部分中,我们介绍了从模板生成函数的三种方式。最后,专门化和重载小节告诉我们如何为特殊类型定制主模板(通过改变其行为)。
有两种方法可以定义函数模板,如下面的代码所示:
template <typename identifier_1, …, typename identifier_n >
function_declaration;
template <class identifier_1,…, class identifier_n>
function_declaration;
这里,identifier_i (i=1,…,n)
是类型或类参数,function_declaration
声明函数体部分。前面两个声明的唯一区别是关键字——一个使用class
而另一个使用typename
,但是两者具有相同的含义和行为。由于一个类型(如基本类型——int、float、double、enum、struct、union 等)不是一个类,引入typename
关键字方法是为了避免混淆。
例如,经典的求最大值函数模板app_max()
,可以声明如下:
template <class T>
T app_max (T a, T b) {
return (a>b?a:b); //note: we use ((a)>(b) ? (a):(b)) in macros
} //it is safe to replace (a) by a, and (b) by b now
这个函数模板可以适用于许多数据类型或类,只要存在 a > b 表达式有效的可复制构造类型。对于用户定义的类,这意味着必须定义大于运算符(>)。
注意,函数模板和模板函数是不同的东西。函数模板是指编译器用来生成函数的一种模板,所以编译器不会为其生成任何目标代码。另一方面,模板函数意味着来自函数模板的实例。由于它是一个函数,相应的目标代码由编译器生成。然而,最新的 C++ 标准文档建议避免使用不精确的术语模板函数。因此,我们将在本书中使用函数模板和成员函数模板。
由于我们可能有无限多的类型和类,函数模板的概念不仅节省了源代码文件中的空间,而且使代码更容易阅读和维护。然而,与为我们的应用中使用的不同数据类型编写单独的函数或类相比,它不会产生更小的目标代码。例如,考虑一个使用浮点和整数版本app_max()
的程序:
cout << app_max<int>(3,5) << endl;
cout << app_max<float>(3.0f,5.0f) << endl;
编译器将在目标文件中生成两个新函数,如下所示:
int app_max<int> ( int a, int b) {
return (a>b?a:b);
}
float app_max<float> (float a, float b) {
return (a>b?a:b);
}
从函数模板声明创建函数新定义的过程称为模板实例化。在这个实例化过程中,编译器确定模板参数,并根据需要为应用生成实际的功能代码。通常有三种形式:显式 实例化**隐式实例化和模板扣除。在接下来的部分中,让我们讨论每种形式。
许多非常有用的 C++ 函数模板可以在不使用显式实例化的情况下编写和使用,但是我们将在这里描述它们,以便您知道如果您需要它们,它们确实存在。首先,让我们看看 C++ 11 之前显式实例化的语法。有两种形式,如下面的代码所示:
template return-type
function_name < template_argument_list > ( function_parameter-list ) ;
template return-type
function_name ( function_parameter_list ) ;
显式实例化定义,也称为指令,强制特定类型的函数模板的实例化,而不考虑将来将调用的模板函数。显式实例化的位置可以在函数模板定义之后的任何地方,并且对于源代码中的给定参数列表,只允许出现一次。
自 C++ 11 以来,显式实例化指令的语法如下。这里我们可以看到extern
关键字加在template
关键字之前:
extern template return-type
function_name < template_argument_list > (function_parameter_list );
(since C++ 11)
extern template return-type
function_name ( function_parameter_list ); (since C++ 11)
使用extern
关键字可以防止该函数模板的隐式实例化(更多细节请参见下一节)。
关于之前声明的app_max()
函数模板,可以使用以下代码显式实例化:
template double app_max<double>(double, double);
template int app_max<int>(int, int);
也可以使用以下代码显式实例化它:
extern template double app_max<double>(double, double);//(since c++ 11)
extren template int app_max<int>(int, int); //(since c++ 11)
这也可以用一种模板论证演绎的方式来完成:
template double f(double, double);
template int f(int, int);
最后,也可以这样做:
extern template double f(double, double); //(since c++ 11)
extern template int f(int, int); //(since c++ 11)
此外,还有一些其他的显式实例化规则。如果您想了解更多,请参考进一步阅读部分【10】了解更多详情。
当一个函数被调用时,该函数的定义需要存在。如果这个函数没有被显式地实例化,那么就达到了隐式实例化的方法,其中模板参数的列表需要被显式地提供或者从上下文中推导出来。以下程序的Part A
提供了本目录中app_max()
隐式实例化的一些示例:
//ch4_2_func_template_implicit_inst.cpp
#include <iostream>
template <class T>
T app_max (T a, T b) { return (a>b?a:b); }
using namespace std;
int main(){
//Part A: implicit instantiation in an explicit way
cout << app_max<int>(5, 8) << endl; //line A
cout << app_max<float>(5.0, 8.0) << endl; //line B
cout << app_max<int>(5.0, 8) << endl; //Line C
cout << app_max<double>(5.0, 8) << endl; //Line D
//Part B: implicit instantiation in an argument deduction way
cout << app_max(5, 8) << endl; //line E
cout << app_max(5.0f, 8.0f) << endl; //line F
//Part C: implicit instantiation in a confuse way
//cout<<app_max(5, 8.0)<<endl; //line G
return 0;
}
第A
、B
、C
和D
行的隐式实例分别是int app_max<int>(int,int)
、float app_max<float>(float, float>)
、int app_max<int>(int,int)
和double app_max<double>(double, double)
。
当您调用模板函数时,编译器需要首先计算出模板参数,即使不是每个模板参数都被指定。大多数时候,它会从函数参数中推导出缺失的模板参数。例如,在前一个函数的 B 部分,当您在第E
行调用app_max(5, 8)
时,编译器将模板参数推导为 int 类型,(int app_max<int>(int,int))
,因为输入参数5
和8
是整数。同样,线F
将推导为浮动类型,即float app_max<float>(float,float)
。
但是,如果在实例化过程中出现混淆,会发生什么呢?例如,在前一个程序的G
的注释行中,根据编译器的不同,它可能会调用app_max<double>(double, double), app_max<int>(int, int)
,或者只是给出一个编译错误消息。帮助编译器推导类型的最好方法是通过显式给出模板参数来调用函数模板。在这种情况下,如果我们叫app_max<double>(5, 8.0)
,任何混乱都会被解决。
From the compiler's point of view, there are several ways to do template argument deduction – deduction from a function call, deduction from a type, auto type deduction, and non-deduced contexts [4]. However, from a programmer's point of view, you should never write fancy code to ill-use the concept of function template deduction to confuse other programmers such as line G in the previous example.
专门化允许我们为一组给定的模板参数定制模板代码。它允许我们为特定的模板参数定义一个特殊的行为。专业化仍然是一个模板;您仍然需要实例化来获取真实的代码(由编译器自动执行)。
在下面的示例代码中,主函数模板T app_max(T a, T b)
将基于运算符 *a > b、*的返回返回a
或b
,但我们可以将其专门化为T = std::string
,这样我们只比较a
和b
的 0 -th 元素;也就是a[0] >b[0]
:
//ch4_3_func_template_specialization.cpp
#include <iostream>
#include <string>
//Part A: define a primary template
template <class T> T app_max (T a, T b) { return (a>b?a:b); }
//Part B: explicit specialization for T=std::string,
template <> std::string app_max<std::string> (std::string a, std::string b){
return (a[0]>b[0]?a:b);
}
//part C: test function
using namespace std;
void main(){
string a = "abc", b="efg";
cout << app_max(5, 6) << endl; //line A
cout << app_max(a, b) << endl; //line B
//question: what's the output if un-comment lines C and D?
//char *x = "abc", *y="efg"; //Line C
//cout << app_max(x, y) << endl; //line D
}
前面的代码先定义了一个主模板,然后明确的将T
专门化为std::string
;也就是不去比较a
和b
的价值观,只关心a[0]
和b[0]
(其中app_max()
的行为是专门化的)。在测试函数中,line A
调用app_max<int>(int,int)
,line B
调用专门化版本,因为在推演的时候没有歧义。如果取消对C
和D
行的注释,将调用主函数模板char* app_max<char > (char*, char*)
,因为char*
和std::string
是不同的数据类型。
*本质上,专门化与函数重载解析有些冲突:编译器需要一种算法,通过在模板和重载函数之间找到正确的匹配来解决这种冲突。选择正确函数的算法包括以下两个步骤:
- 在常规函数和非专用模板之间执行重载解析。
- 如果选择了非专用模板,请检查是否存在更适合它的专用模板。
例如,在下面的代码块中,我们声明了主函数(line 0
)和专用函数模板(lines 1-4
),以及重载函数(f()
的lines 5-6)
):
template<typename T1, typename T2> void f( T1, T2 );// line 0
template<typename T> void f( T ); // line 1
template<typename T> void f( T, T ); // line 2
template<typename T> void f( int, T* ); // line 3
template<> void f<int>( int ); // line 4
void f( int, double ); // line 5
void f( int ); // line 6
f()
将在下面的代码块中被多次调用。基于前面的两步规则,我们可以在注释中显示选择了哪个函数。我们将在下面解释这样做的原因:
int i=0;
double d=0;
float x=0;
complex<double> c;
f(i); //line A: choose f() defined in line 6
f(i,d); //line B: choose f() defined in line 5
f<int>(i); //line C: choose f() defined in line 4
f(c); //line D: choose f() defined in line 1
f(i,i); //line E: choose f() defined in line 2
f(i,x); //line F: choose f() defined in line 0
f(i, &d); //line G: choose f() defined in line 3
对于lines A
和line B
,由于lines 5
和line 6
中定义的f()
是常规函数,所以它们的优先级最高,所以f(i)
和f(i,d)
会分别选择它们。对于line C
,因为有专门的模板存在,所以从line 4
生成的f()
比从line 1
创建的更加匹配。对于line D
,由于c
是complex<double>
类型,只有line 1
中定义的主功能模板匹配。Line E
会选择line 2
创建的f()
,因为两个输入变量是同一类型。最后,lines F
和line G
将分别在0
和3
行中拾取从模板创建的功能。
了解了功能模板之后,我们现在将继续讨论类模板。
类模板定义了一个类家族,它经常被用来实现一个容器。例如,C++ 标准库包含许多类模板,如std::vector
、std::map
、std::deque
等。在 OpenCV 中,cv::Mat
是一个非常强大的类模板,它可以处理 1D、2D 和内置数据类型的 3D 矩阵或图像,如int8_t
、uint8_t
、int16_t
、uint16_t
、int32_t
、uint32_t
、float
、double
等。
类似于函数模板,如下图所示,类模板的概念包含模板创建语法、其专门化及其隐式和显式实例化:
在上图的第一部分中,通过一定的语法格式,我们可以为泛型类型创建一个类模板,也称为主模板,可以为具有不同成员函数和/或变量的特殊类型进行定制。一旦我们有了类模板,在第二部分中,编译器将根据应用的需求显式或隐式地将其实例化为模板类。
现在,让我们看看创建类模板的语法。
创建类模板的语法如下:
[export] template <template_parameter_list> class-declaration
这里,我们有以下内容:
template_parameter-list
(参见中的链接,进一步阅读上下文【10】)是模板参数的非空逗号分隔列表,每个模板参数都是非类型参数、类型参数、模板参数或其中任何一个的参数包。class-declaration
是用来声明一个类的部分,这个类包含一个类名和它在花括号中的主体。通过这样做,声明的类名也变成了模板名。
例如,我们可以定义一个类模板V
,使其包含各种 1D 数据类型:
template <class T>
class V {
public:
V( int n = 0) : m_nEle(n), m_buf(0) { creatBuf();}
~V(){ deleteBuf(); }
V& operator = (const V &rhs) { /* ... */}
V& operator = (const V &rhs) { /* ... */}
T getMax(){ /* ... */ }
protected:
void creatBuf() { /* ... */}
void deleteBuf(){ /* ... */}
public:
int m_nEle;
T * m_buf;
};
一旦我们有了这个类模板,编译器就可以在实例化过程中生成类。由于我们在函数模板小节中提到的原因,我们将避免在本书中使用不精确的术语template
类。相反,我们将使用类模板。
考虑到我们在上一节中定义的类模板V
,我们将假设后面会出现以下声明:
V<char> cV;
V<int> iV(10);
V<float> fV(5);
然后,编译器将创建V
类的三个实例,如下所示:
class V<char>{
public:
V(int n=0);
// ...
public:
int m_nEle;
char *m_buf;
};
class V<int>{
public:
V(int n=0);
// ...
public:
int m_nEle;
int *m_buf;
};
class V<float>{
public:
V(int n = 0);
// ...
public:
int m_nEle;
float *m_buf;
};
类似于函数模板实例化,类模板实例化有两种形式——显式实例化和隐式实例化。让我们看看他们。
显式实例化的语法如下:
template class template_name < argument_list >;
extern template class template_name < argument_list >;//(since C++ 11)
显式实例化定义强制实例化它们引用的类、结构或联合。在 C++ 0x 标准中,模板专门化或其成员的隐式实例化被抑制。类似于函数模板的显式实例化,这种显式实例化的位置可以在其模板定义之后的任何地方,并且只允许在一个文件中的整个程序中定义一次。
此外,由于 C++ 11,隐式实例化步骤将被显式实例化声明(外部模板)绕过。这可以用来减少编译时间。
回到模板类V
,我们可以如下显式实例化它:
template class V<int>;
template class V<double>;
或者,我们可以执行以下操作(从 C++ 11 开始):
extern template class V<int>;
extern template class V<double>;
如果我们显式实例化一个函数或类模板,但程序中没有相应的定义,编译器将向我们显示一条错误消息,如下所示:
//ch4_4_class_template_explicit.cpp
#include <iostream>
using namespace std;
template <typename T> //line A
struct A {
A(T init) : val(init) {}
virtual T foo();
T val;
}; //line B
//line C
template <class T> //T in this line is template parameter
T A<T>::foo() { //the 1st T refers to function return type,
//the T in <> specifies that this function's template
//parameter is also the class template parameter
return val;
} //line D
extern template struct A<int>; //line E
#if 0 //line F
int A<int>::foo() {
return val+1;
}
#endif //line G
int main(void) {
A<double> x(5);
A<int> y(5);
cout<<"fD="<<x.foo()<<",fI="<<y.foo()<< endl;
return 0; //output: fD=5,fI=6
}
在前面的代码块中,我们在行 A 和行 B 之间定义了一个类模板,然后我们实现了它的成员函数foo()
,从lines C
到line D
。接下来,我们在line E
为int
类型显式实例化它。由于lines F
和line G
之间的代码块被注释掉了(这意味着对于这个显式的int
类型实例化没有foo()
的相应定义),所以我们有一个链接错误。要解决这个问题,我们需要在line F
用#if 1
代替#if 0
。
最后,显式实例化声明还有一些附加限制,如下所示:
- Static :静态类成员可以命名,但是在显式实例化声明中不能允许静态函数。
- 内联:内联函数在显式实例化声明中没有效果,内联函数是隐式实例化的。
- 类及其成员:对于显式实例化一个类及其所有成员来说是不等价的。
当引用一个模板类时,如果它没有被显式实例化或显式专门化,编译器将只根据需要从它的模板生成代码。这叫做隐式实例化,其语法如下:
class_name<argument list> object_name; //for non-pointer object
class_name<argument list> *p_object_name; //for pointer object
对于非指针对象,将实例化一个模板类并创建其对象,但只生成该对象使用的成员函数。对于指针对象,除非在程序中使用了成员,否则它不会被实例化。
考虑下面的例子,我们在ch4_5_class_template_implicit_inst.h
文件中定义了一个类模板X
:
//file ch4_5_class_template_implicit_inst.h
#ifndef __CH4_5_H__
#define __CH4_5_H__
#include <iostream>
template <class T>
class X {
public:
X() = default;
~X() = default;
void f() { std::cout << "X::f()" << std::endl; };
void g() { std::cout << "X::g()" << std::endl; };
};
#endif
然后,它包含在以下四个cpp
文件中,每个文件中有ain()
:
//file ch4_5_class_template_implicit_inst_A.cpp
#include "ch4_5_class_template_implicit_inst.h"
void main()
{
//implicit instantiation generates class X<int>, then create object xi
X<int> xi ;
//implicit instantiation generates class X<float>, then create object xf
X<float> xf;
return 0;
}
在ch4_5_class_template_implicit_inst_A.cpp
中,编译器会隐式实例化X<int>
和X<float>
类,然后创建xi
和xf
对象。但是由于没有使用X::f()
和X::g()
,所以没有实例化。
现在,我们来看看ch4_5_class_template_implicit_inst_B.cpp
:
//file ch4_5_class_template_implicit_inst_B.cpp
#include "ch4_5_class_template_implicit_inst.h"
void main()
{
//implicit instantiation generates class X<int>, then create object xi
X<int> xi;
xi.f(); //and generates function X<int>::f(), but not X<int>::g()
//implicit instantiation generates class X<float>, then create object
//xf and generates function X<float>::g(), but not X<float>::f()
X<float> xf;
xf.g() ;
}
这里,编译器将隐式实例化X<int>
类,创建xi
对象,然后生成X<int>::f()
函数,但不生成X<int>::g()
。同样,它将实例化X<float>
类,创建xf
对象,并生成X<float>::g()
函数,但不生成X<float>::f()
。
然后,我们有ch4_5_class_template_implicit_inst_C.cpp
:
//file ch4_5_class_template_implicit_inst_C.cpp
#include "ch4_5_class_template_implicit_inst.h"
void main()
{
//inst. of class X<int> is not required, since p_xi is pointer object
X<int> *p_xi ;
//inst. of class X<float> is not required, since p_xf is pointer object
X<float> *p_xf ;
}
由于p_xi
和p_xf
是指针对象,所以不需要通过编译器实例化它们对应的模板类。
最后,我们有ch4_5_class_template_implicit_inst_D.cpp
:
//file ch4_5_class_template_implicit_inst_D.cpp
#include "ch4_5_class_template_implicit_inst.h"
void main()
{
//inst. of class X<int> is not required, since p_xi is pointer object
X<int> *p_xi;
//implicit inst. of X<int> and X<int>::f(), but not X<int>::g()
p_xi = new X<int>();
p_xi->f();
//inst. of class X<float> is not required, since p_xf is pointer object
X<float> *p_xf;
p_xf = new X<float>();//implicit inst. of X<float> occurs here
p_xf->f(); //implicit inst. X<float>::f() occurs here
p_xf->g(); //implicit inst. of X<float>::g() occurs here
delete p_xi;
delete p_xf;
}
这将隐式实例化X<int>
和X<int>::f()
,但不会实例化X<int>::g()
;同样,对于X<float>
,X<float>::f()
和X<float>::g()
将被实例化。
与函数专门化类似,当特定类型作为模板参数传递时,类模板的显式专门化为主模板定义了不同的实现。但是,它仍然是一个类模板,您需要通过实例化来获取真正的代码。
例如,假设我们有一个struct X
模板,可以存储任何数据类型的一个元素,它只有一个名为increase()
的成员函数。但是对于 char 类型的数据,我们想要一个不同的increase()
实现,并且需要添加一个名为toUpperCase()
的新成员函数。因此,我们决定为该类型声明一个类模板专门化。我们按如下方式进行:
- 声明主类模板:
template <typename T>
struct X {
X(T init) : m(init) {}
T increase() { return ++ m; }
T m;
};
这个步骤声明了一个主类模板,其中它的构造函数初始化m
成员变量,increase()
给m
加一并返回它的值。
- 接下来,我们需要对 char 类型数据执行专门化:
template <> //Note: no parameters inside <>, it tells compiler
//"hi i am a fully specialized template"
struct X<char> { //Note: <char> after X, tells compiler
// "Hi, this is specialized only for type char"
X(char init) : m(init) {}
char increase() { return (m<127) ? ++ m : (m=-128); }
char toUpperCase() {
if ((m >= 'a') && (m <= 'z')) m += 'A' - 'a';
return m;
}
char m;
};
该步骤创建了一个专门的(相对于主类模板)类模板,该模板带有一个附加的成员函数toUpperCase()
,仅用于 char 类型数据。
- 现在,我们运行一个测试:
int main() {
X<int> x1(5); //line A
std::cout << x1.increase() << std::endl;
X<char> x2('b'); //line B
std::cout << x2.toUpperCase() << std::endl;
return 0;
}
最后,我们有一个main()
函数来测试它。在第 A 行中,x1
是一个已经从主模板X<T>
*隐式实例化的对象。*由于x1.m
的初始值为5
,因此6
将从x1.increase()
返回。在line B
中,x2
是从专门化模板X<char>
实例化的对象,x2.m
的值在执行时为b
。调用x2.toUpperCase()
后,B
将是返回值。
The complete code for this example can be found at ch4_6_class_template_specialization.cpp
.
总之,类模板显式专门化中使用的语法如下:
template <> class[struct] class_name<template argument list> { ... };
这里,空模板参数列表template <>
用于将其明确声明为模板专门化,<template argument list>
是要专门化的类型参数。例如,在ex4_6_class_template_specialization.cpp
中,我们使用以下内容:
template <> struct X<char> { ... };
在这里,X
之后的<char>
标识了我们要为其声明模板类专门化的类型。
此外,当我们对一个模板类进行专门化时,它的所有成员——甚至那些在主模板中相同的成员——都必须被定义,因为在模板专门化过程中主模板没有继承概念。
接下来,我们将了解部分专业化。这是显式专门化的一般陈述。与只有模板参数列表的显式专门化的格式相比,部分专门化需要模板参数列表和参数列表。对于模板实例化,如果用户的模板参数列表与模板参数的子集匹配,编译器将选择部分专门化模板。然后,编译器将根据部分专门化模板生成一个新的类定义。
在下面的例子中,对于主类模板A
,我们可以在参数列表中将它部分专门化为常量T
。注意两者的参数表相同,都是<typename T>
:
//primary class template A
template <typename T> class A{ /* ... */ };
//partial specialization for const T
template <typename T> class A<const T>{ /* ... */ };
在下面的例子中,主类模板B
有两个参数:<typename T1
和typename T2 >
。我们通过T1=int
对其进行部分专门化,保持T2
不变:
//primary class template B
template <typename T1, typename T2> class B{ /* ... */ };
//partial specialization for T1 = int
template <typename T2> class B<int, T2>{ /* ... */};
最后,在下面的示例中,我们可以看到部分专门化中的模板参数数量不必与原始主模板中出现的参数数量相匹配。但是,模板参数的数量(出现在尖括号中类名的后面)必须与主模板中参数的数量和类型相匹配:
//primary class template C: template one parameter
template <typename T> struct C { T type; };
//specialization: two parameters in parameter list
//but still one argument (<T[N]>) in argument list
template <typename T, int N> struct C<T[N]>
{T type; };
同样,类模板部分专门化仍然是类模板。您必须分别为其成员函数和数字变量提供定义。
结束这一部分,让我们总结一下到目前为止我们所学到的东西。在下表中,您可以看到函数和类模板、它们的实例化和专门化之间的比较:
| | 功能模板 | 类模板 | 评论 |
| 申报 | template <class T1, class T2>``void f(T1 a, T2 b) { ... }
| template <class T1, class T2>``class X { ... };
| 该声明定义了一个名为模板参数的函数/类模板<class T1, class T2>
。 |
| 明确的实例化 | template void f <int, int >( int, int);
或者外部模板void f <int, int >( int, int);
(从 C++ 11 开始) | template class X<int, float>;
或者extern template class X<int,float>;
(从 C++ 11 开始) | 实例化之后,现在有了函数/类,但它们被称为模板函数/类。 |
| 含蓄的实例化 | {...f(3, 4.5);``f<char, float>(120, 3.14);
} | {...X<int,float> obj;``X<char, char> *p;
} | 当函数调用或类对象/指针被声明时,如果它没有被显式实例化,则使用隐式实例化方法。 |
| 专门化 | template <>``void f<int,float>(int a, float b)``{ ... }
| template <>``class X <int, float>{ ... };
| 主模板的完全定制版本(无参数列表)仍需要实例化。 |
| 部分专业化 | template <class T>``void f<T,T>(T a, T b)``{ ... }
| template <class T>``class X <T, T>{ ... };
| 主模板的部分定制版本(具有参数列表)仍然需要实例化。 |
这里需要强调五个概念:
- 声明:我们需要遵循用于定义函数或类模板的语法。此时,函数或类模板本身不是类型、函数或任何其他实体。换句话说,源文件中只有模板定义,没有生成可以编译成目标文件的代码。
- 隐式实例化:任何代码要出现,必须实例化一个模板。在这个过程中,必须确定模板参数,这样编译器才能生成实际的函数或类。换句话说,它们是按需编译的,这意味着在给出具有特定模板参数的实例化之前,不会编译模板函数或类的代码。
- 显式实例化:告诉编译器用给定的类型实例化模板,不管是否使用它们。通常,它用于提供库。
- 【全特殊化】 :这个没有参数表(全定制);它只有一个参数列表。模板专门化最有用的一点是,您可以为特定的类型参数创建特殊的模板。
- 部分特殊化:这和完全特殊化类似,只是零件参数表(部分定制)和零件实参表。
在前一节中,我们学习了如何用固定数量的类型参数编写函数或类模板。但是自从 C++ 11 以来,标准的泛型函数和类模板可以接受可变数量的类型参数。这叫做变量模板,是进一步阅读上下文【6】中 C++ 的扩展。我们将通过查看示例来了解变量模板的语法和用法。
如果函数或类模板采用零个或多个参数,可以定义如下:
//a class template with zero or more type parameters
template <typename... Args> class X { ... };
//a function template with zero or more type parameters
template <typename... Args> void foo( function param list) { ...}
这里,<typename ... Args>
声明了一个参数包。注意这里,Args
不是关键词;您可以使用任何有效的变量名。前面的类/函数模板可以采用任意数量的typename
作为需要实例化的参数,如下所示:
X<> x0; //with 0 template type argument
X<int, std::vector<int> > x1; //with 2 template type arguments
//with 4 template type arguments
X<int, std::vector<int>, std::map<std::string, std::vector<int>>> x2;
//with 2 template type arguments
foo<float, double>( function argument list );
//with 3 template type arguments
foo<float, double, std::vector<int>>( function argument list );
如果变量模板至少需要一个类型参数,则使用以下定义:
template <typename A, typename... Rest> class Y { ... };
template <typename A, typename... Rest>
void goo( const int a, const float b) { ....};
同样,我们可以使用以下代码实例化它们:
Y<int > y1;
Y<int, std::vector<int>, std::map<std::string, std::vector<int>>> y2;
goo<int, float>( const int a, const float b );
goo<int,float, double, std::vector<int>>( const int a, const float b );
在前面的代码中,我们分别使用一个和三个模板参数,从变量类模板的实例化创建了y1
和y2
对象。对于变量函数goo
模板,我们将其实例化为两个模板函数,分别具有两个和三个模板参数。
下面可能是最简单的例子,显示了一个变量模板,用于查找任何输入参数列表的最小值。这个例子使用递归的概念,直到到达my_min(double n)
退出:
//ch4_7_variadic_my_min.cpp
//Only tested on g++ (Ubuntu/Linaro 7.3.0-27 ubuntu1~18.04)
//It may have compile errors for other platforms
#include <iostream>
#include <math.h>
double my_min(double n){
return n;
}
template<typename... Args>
double my_min(double n, Args... args){
return fmin(n, my_min(args...));
}
int main() {
double x1 = my_min(2);
double x2 = my_min(2, 3);
double x3 = my_min(2, 3, 4, 5, 4.7,5.6, 9.9, 0.1);
std::cout << "x1="<<x1<<", x2="<<x2<<", x3="<<x3<<std::endl;
return 0;
}
printf()
变量函数可能是 C 或 C++ 中最有用、最强大的函数之一;然而,它不是类型安全的。在下面的代码块中,我们采用了经典的类型安全printf()
示例来演示变量模板的有用性。一如既往,首先,我们需要定义一个基函数,void printf_vt(const char *s)
,结束递归:
//ch4_8_variadic_printf.cpp part A: base function - recursive end
void printf_vt(const char *s)
{
while (*s){
if (*s == '%' && *(++ s) != '%')
throw std::runtime_error("invalid format string: missing arguments");
std::cout << *s++ ;
}
}
然后,在其变量模板函数printf_vt()
中,每当%
被命中时,该值被打印,其余的被传递到其递归,直到到达基函数:
//ch4_8_variadic_printf.cpp part B: recursive function
template<typename T, typename... Rest>
void printf_vt(const char *s, T value, Rest... rest)
{
while (*s) {
if (*s == '%' && *(++ s) != '%') {
std::cout << value;
printf_vt(s, rest...); //called even when *s is 0,
return; //but does nothing in that case
}
std::cout << *s++ ;
}
}
最后,我们可以使用以下代码测试并与传统的printf()
进行比较:
//ch4_8_variadic_printf.cpp Part C: testing
int main() {
int x = 10;
float y = 3.6;
std::string s = std::string("Variadic templates");
const char* msg1 = "%s can accept %i parameters (or %s), x=%d, y=%f\n";
printf(msg1, s, 100, "more",x,y); //replace 's' by 's.c_str()'
//to prevent the output bug
const char* msg2 = "% can accept % parameters (or %); x=%,y=%\n";
printf_vt(msg2, s, 100, "more",x,y);
return 0;
}
前面代码的输出如下:
p.]�U can accept 100 parameters (or more), x=10, y=3.600000
Variadic templates can accept 100 parameters (or more); x=10,y=3.6
在第一行的开头,我们可以看到一些来自printf()
的 ASCII 字符,因为%s
对应的变量类型应该是一个指向字符的指针,但是我们给了它一个类型std::string
。要解决这个问题,我们需要通过s.c_str()
。但是,使用可变模板版本功能,我们没有这个问题。而且,我们只需要提供%
,这就更好了——至少,是为了这个实现。
总之,本节简要介绍了变量模板及其应用。变量模板提供了以下好处(从 C++ 11 开始):
- 它是模板族的轻量级扩展。
- 它展示了在不使用难看的模板和预处理器宏的情况下实现大量模板库的能力。因此,实现代码能够被理解和调试,并且还节省了编译时间。
- 它支持
printf()
变量函数的类型安全实现。
接下来,我们将探讨模板参数和参数。
在前两节中,我们学习了函数和类模板及其实例化。我们知道,在定义一个模板时,需要给出它的参数列表。当我们实例化它时,必须提供相应的参数列表。在本节中,我们将进一步研究这两个列表的分类和细节。
回想一下下面的语法,它用于定义类/函数模板。template
关键字后有一个<>
符号,其中必须给出一个或多个模板参数:
//class template declaration
template <*parameter-list*> class-declaration
//function template declaration
template <parameter-list> function-declaration
参数列表中的参数可以是以下三种类型之一:
Non-type template parameter
:指引用静态实体的编译时常量值,如整数和指针。这些通常被称为非类型参数。Type template parameter
:指内置类型名或用户自定义类。Template template parameter
:表示参数为其他模板。
我们将在下面的小节中更详细地讨论这些问题。
非类型模板参数的语法如下:
//for a non-type template parameter with an optional name
type name(optional)
//for a non-type template parameter with an optional name
//and a default value
type name(optional)=default
//For a non-type template parameter pack with an optional name
type ... name(optional) (since C++ 11)
这里,type
是以下类型之一——整型、枚举、指向对象或函数的指针、lvalue
对对象或函数的引用、指向成员对象或成员函数的指针、std::nullptr_t
(从 C++ 11 开始)。此外,我们可以将数组和/或函数类型放在模板声明中,但是它们会被数据和/或函数指针自动替换。
以下示例显示了使用非类型模板参数int N
的类模板。在main()
中,我们实例化并创建一个对象,x
,因此x.a
有五个元素,初始值为1
。将第四个元素值设置为10
后,我们打印输出:
//ch4_9_none_type_template_param1.cpp
#include <iostream>
template<int N>
class V {
public:
V(int init) {
for (int i = 0; i<N; ++ i) { a[i] = init; }
}
int a[N];
};
int main()
{
V<5> x(1); //x.a is an array of 5 int, initialized as all 1's
x.a[4] = 10;
for( auto &e : x.a) {
std::cout << e << std::endl;
}
}
以下是使用const char*
作为非类型模板参数的函数模板示例:
//ch4_10_none_type_template_param2.cpp
#include <iostream>
template<const char* msg>
void foo() {
std::cout << msg << std::endl;
}
// need to have external linkage
extern const char str1[] = "Test 1";
constexpr char str2[] = "Test 2";
extern const char* str3 = "Test 3";
int main()
{
foo<str1>(); //line 1
foo<str2>(); //line 2
//foo<str3>(); //line 3
const char str4[] = "Test 4";
constexpr char str5[] = "Test 5";
//foo<str4>(); //line 4
//foo<str5>(); //line 5
return 0;
}
在main()
中,我们成功地用str1
和str2
实例化了foo()
,因为它们都是编译时常量值,并且有外部链接。然后,如果我们取消第 3-5 行的注释,编译器将报告错误消息。出现这些编译器错误的原因如下:
- 第 3 行 :
str3
不是常量变量,因此str3
指向的值不能更改。然而str3
的价值是可以改变的。 - 第 4 行 :
str4
不是const char*
类型的有效模板参数,因为它没有链接。 - 第 5 行 :
str5
不是const char*
类型的有效模板参数,因为它没有链接。
非类型参数的另一个最常见的用法是数组的大小。如果您想了解更多,请前往https://stackoverflow.com/questions/33234979。
类型模板参数的语法如下:
//A type Template Parameter (TP) with an optional name
typename |class name(optional)
//A type TP with an optional name and a default
typename[class] name(optional) = default
//A type TP pack with an optional name
typename[class] ... name(optional) (since C++ 11)
**Note:**Here, we use the typename
and class
keywords interchangeably. Inside the body of the template declaration, the name of a type parameter is a typedef-name
. When the template is instantiated, it aliases the type supplied.
现在,让我们看一些例子:
- 没有默认值的类型模板参数:
Template<class T> //with name
class X { /* ... */ };
Template<class > //without name
class Y { /* ... */ };
- 默认的类型模板参数:
Template<class T = void> //with name
class X { /* ... */ };
Template<class = void > //without name
class Y { /* ... */ };
- 类型模板参数包:
template<typename... Ts> //with name
class X { /* ... */ };
template<typename... > //without name
class Y { /* ... */ };
这个模板参数包可以接受零个或更多的模板参数,并且它只在 C++ 11 上起作用。
模板模板参数的语法如下:
//A template template parameter with an optional name
template <parameter-list> class *name*(optional)
//A template template parameter with an optional name and a default
template <parameter-list> class *name*(optional) = default
//A template template parameter pack with an optional name
template <parameter-list> class ... *name*(optional) (since C++ 11)
Note: In template template parameter declaration, only the class
keyword can be used; typename
is not allowed. In the body of the template declaration, the name of a parameter is a template-name
, and we need arguments to instantiate it.
现在,假设您有一个函数充当对象列表的流输出运算符:
template<typename T>
static inline std::ostream &operator << ( std::ostream &out,
std::list<T> const& v)
{
/*...*/
}
从前面的代码中,您可以看到,对于向量、双端队列和多种映射类型等序列容器,它们是相同的。因此,使用模板模板参数的概念,可以有一个操作符<<
来统治它们。这方面的一个例子可以在exch4_tp_c.cpp
中找到:
/ch4_11_template_template_param.cpp (courtesy: https://stackoverflow.com/questions/213761)
#include <iostream>
#include <vector>
#include <deque>
#include <list>
using namespace std;
template<class T, template<class, class...> class X, class... Args>
std::ostream& operator <<(std::ostream& os, const X<T, Args...>& objs) {
os << __PRETTY_FUNCTION__ << ":" << endl;
for (auto const& obj : objs)
os << obj << ' ';
return os;
}
int main() {
vector<float> x{ 3.14f, 4.2f, 7.9f, 8.08f };
cout << x << endl;
list<char> y{ 'E', 'F', 'G', 'H', 'I' };
cout << y << endl;
deque<int> z{ 10, 11, 303, 404 };
cout << z << endl;
return 0;
}
前面程序的输出如下:
class std::basic_ostream<char,struct std::char_traits<char> > &__cdecl operator
<<<float,class std::vector,class std::allocator<float>>(class std::basic_ostream
<char,struct std::char_traits<char> > &,const class std::vector<float,class std:
:allocator<float> > &):
3.14 4.2 7.9 8.08
class std::basic_ostream<char,struct std::char_traits<char> > &__cdecl operator
<<<char,class std::list,class std::allocator<char>>(class std::basic_ostream<cha
r,struct std::char_traits<char> > &,const class std::list<char,class std::alloca
tor<char> > &):
E F G H I
class std::basic_ostream<char,struct std::char_traits<char> > &__cdecl operator
<<<int,class std::deque,class std::allocator<int>>(class std::basic_ostream<char
,struct std::char_traits<char> > &,const class std::deque<int,class std::allocat
or<int> > &):
10 11 303 404
不出所料,每次调用的输出的第一部分是pretty
格式的模板函数名,而第二部分输出每个容器的元素值。
要实例化一个模板,所有的模板参数必须用它们相应的模板参数替换。参数要么是显式提供的,要么是从初始值设定项(对于类模板)推导出的,要么是从上下文(对于函数模板)推导出的,要么是默认的。由于有三类模板参数,我们也将有三个相应的模板参数。这些是模板非类型参数、模板类型参数和模板模板参数*。*除了这些,我们还将讨论默认模板参数。
回想一下,非类型模板参数指的是编译时常量值,如整数、指针和对静态实体的引用。模板参数列表中提供的非类型模板参数必须与这些值之一匹配。通常,非类型模板参数用于类初始化或类容器的大小规范。
虽然对非类型实参的每种类型(整数和算术类型、指向对象/函数/成员的指针、lvalue
引用参数等)的详细规则的讨论超出了本书的范围,但总的一般规则是模板非类型实参应该转换为相应模板参数的常量表达式。
现在,让我们看看下面的例子:
//part 1: define template with non-type template parameters
template<const float* p> struct U {}; //float pointer non-type parameter
template<const Y& b> struct V {}; //L-value non-type parameter
template<void (*pf)(int)> struct W {};//function pointer parameter
//part 2: define other related stuff
void g(int,float); //declare function g()
void g(int); //declare an overload function of g()
struct Y { //declare structure Y
float m1;
static float m2;
};
float a[10];
Y y; //line a: create a object of Y
//part 3: instantiation template with template non-type arguments
U<a> u1; //line b: ok: array to pointer conversion
U<&y> u2; //line c: error: address of Y
U<&y.m1> u3; //line d: error: address of non-static member
U<&y.m2> u4; //line e: ok: address of static member
V<y> v; //line f: ok: no conversion needed
W<&g> w; //line g: ok: overload resolution selects g(int)
在前面的代码中,在part 1
中,我们定义了三个具有不同非类型模板参数的模板结构。然后,在part 2
中,我们声明了两个重载函数和struct Y
。最后,在part 3
中,我们研究了通过不同的非类型参数实例化它们的正确方法。
与模板非类型参数相比,模板类型参数(对于类型模板参数)的规则很简单,要求必须是typeid
。这里,typeid
是一个标准的 C++ 运算符,在运行时返回类型标识信息。它基本上返回一个可以和其他type_info
对象比较的type_info
对象。
现在,让我们看看下面的例子:
//ch4_12_template_type_argument.cpp
#include <iostream>
#include <typeinfo>
using namespace std;
//part 1: define templates
template<class T> class C {};
template<class T> void f() { cout << "T" << endl; };
template<int i> void f() { cout << i << endl; };
//part 2: define structures
struct A{}; // incomplete type
typedef struct {} B; // type alias to an unnamed type
//part 3: main() to test
int main() {
cout << "Tid1=" << typeid(A).name() << "; ";
cout << "Tid2=" << typeid(A*).name() << "; ";
cout << "Tid3=" << typeid(B).name() << "; ";
cout << "Tid4=" << typeid(int()).name() << endl;
C<A> x1; //line A: ok,'A' names a type
C<A*> x2; //line B: ok, 'A*' names a type
C<B> x3; //line C: ok, 'B' names a type
f<int()>(); //line D: ok, since int() is considered as a type,
//thus calls type template parameter f()
f<5>(); //line E: ok, this calls non-type template parameter f()
return 0;
}
在本例中,在part 1
中,我们定义了三个类和函数模板:类模板 C 及其类型模板参数,两个函数模板分别具有一个类型模板参数和一个非类型模板参数。在part 2
中,我们有一个不完整的struct A
和一个未命名的类型struct B
。最后,在part 3
我们测试了它们。Ubuntu 18.04 中四个typeid()
的输出如下:
Tid1=A; Tid2=P1A; Tid3=1B; Tid4=FivE
从 x86 MSVC 版本 19.24,我们有以下内容:
Tid1=struct A; Tid2=struct A; Tid3=struct B; Tid4=int __cdecl(void)
此外,由于A
、A*、B
和int()
具有类型标识,所以从行 A 到 D 的代码段与模板类型类或函数链接。非类型模板参数函数模板只实例化 E 行,即f()
。
对于模板模板参数,其对应的模板参数是类模板的名称或模板别名。在查找与模板模板参数匹配的模板时,只考虑主类模板。
这里,主模板指的是被专门化的模板。即使它们的参数列表可能匹配,编译器也不会考虑模板模板参数的任何部分专门化。
以下是模板模板参数的示例:
//ch4_13_template_template_argument.cpp
#include <iostream>
#include <typeinfo>
using namespace std;
//primary class template X with template type parameters
template<class T, class U>
class X {
public:
T a;
U b;
};
//partially specialization of class template X
template<class U>
class X<int, U> {
public:
int a; //customized a
U b;
};
//class template Y with template template parameter
template<template<class T, class U> class V>
class Y {
public:
V<int, char> i;
V<char, char> j;
};
Y<X> c;
int main() {
cout << typeid(c.i.a).name() << endl; //int
cout << typeid(c.i.b).name() << endl; //char
cout << typeid(c.j.a).name() << endl; //char
cout << typeid(c.j.b).name() << endl; //char
return 0;
}
在这个例子中,我们定义了一个主类模板X
及其专门化,然后是一个类模板Y
,带有一个模板模板参数。接下来,我们使用模板参数X
隐式实例化Y
,并创建一个对象c
。最后main()
输出四个typeid()
的名称,结果分别为int
、char
、char
和char
。
在 C++ 中,通过传递参数来调用函数,参数由函数使用。如果在调用函数时没有传递参数,则使用默认值。类似于函数参数默认值,模板参数可以有默认参数。定义模板时,我们可以设置它的默认参数,如下所示:
/ch4_14_default_template_arguments.cpp //line 0
#include <iostream> //line 1
#include <typeinfo> //line 2
template<class T1, class T2 = int> class X; //line 3
template<class T1 = float, class T2> class X;//line 4
template<class T1, class T2> class X { //line 5
public: //line 6
T1 a; //line 7
T2 b; //line 8
}; //line 9
using namespace std;
int main() {
X<int> x1; //<int,int>
X<float>x2; //<float,int>
X<>x3; //<float,int>
X<double, char> x4; //<double, char>
cout << typeid(x1.a).name() << ", " << typeid(x1.b).name() << endl;
cout << typeid(x2.a).name() << ", " << typeid(x2.b).name() << endl;
cout << typeid(x3.a).name() << ", " << typeid(x3.b).name() << endl;
cout << typeid(x4.a).name() << ", " << typeid(x4.b).name() << endl;
return 0
}
当我们设置模板参数的默认参数时,需要遵循某些规则:
- 声明顺序很重要–默认模板参数的声明必须位于主模板声明的顶部。例如,在前面的示例中,您不能将第 3 行和第 4 行的代码移到第 9 行之后。
- 如果一个参数有默认参数,那么它后面的所有参数也必须有默认参数。例如,以下代码不正确:
template<class U = char, class V, class W = int> class X { }; //Error
template<class V, class U = char, class W = int> class X { }; //OK
- 不能在同一范围内给同一个参数两次默认参数。例如,如果使用以下代码,您将收到一条错误消息:
template<class T = int> class Y;
//compiling error, to fix it, replace "<class T = int>" by "<class T>"
template<class T = int> class Y {
public: T a;
};
这里我们讨论了两个列表:template_parameter_list
和template_argument_list
。这些分别用于函数或类模板的创建和实例化*、**。*
我们还了解了另外两个重要的规则:
- 当我们定义一个类或函数模板时,我们需要给出它的
template_parameter_list
:
template <template_parameter_list>
class X { ... }
template <template_parameter_list>
void foo( function_argument_list ) { ... } //assume return type is void
- 当我们实例化它们时,我们必须提供相应的
argument_list
:
class X<template_argument_list> x
void foo<template_argument_list>( function_argument_list )
这两个列表中的参数或参数类型可以分为三类,如下表所示。请注意,虽然顶行是类模板,但这些属性也适用于函数模板:
| | 定义模板时****模板 <模板 _ 参数 _ 列表>类 X <.../> | 实例化模板时****类 X <模板 _ 参数 _ 列表> x | | 非类型 | 此参数列表中的实体可以是下列之一:
- 积分或枚举
- 指向对象或函数的指针
lvalue
对对象的引用或lvalue
对功能的引用- 指向成员的指针
- C++ 11 标准
::nullptr_t
C++ 11 结束
|
- Non-typed parameters in this list are expressions whose values can be determined at compile time.
- Such parameters must be constant expressions, addresses of functions or objects with external links, or addresses of static class members.
- Non-type parameters are usually used to initialize classes or specify the size of class members.
| | 类型 | 此参数列表中的实体可以是下列之一:
- 必须以 typename 或 class 开头。
- 在模板声明的主体中,类型参数的名称是
typedef-name
。当模板被实例化时,它为所提供的类型取别名。
|
- The type of argument must have a
typeid
. - It cannot be a local type, an unlinked type, an unnamed type or a combination of any of these types.
| | 模板 | 此参数列表中的实体可以是下列之一:
template <parameter-list>
类名template <parameter-list>
类...名称(可选)(从 C++ 11 开始)
| 此列表中的模板参数是类模板的名称。 |
在接下来的部分中,我们将探索如何在 C++ 中实现特征,并使用它们优化算法。
泛型编程意味着编写在特定要求下可以处理任何数据类型的代码。这是软件工程行业中交付可重用高质量代码的最有效方式。然而,在泛型编程中,有时泛型还不够好。每当类型之间的差异过于复杂时,高效的泛型就很难优化公共实现。例如,在实现一个排序函数模板时,如果我们知道参数类型是一个链表而不是一个数组,那么将会实现一个不同的策略来优化性能。
虽然模板专门化是克服这个问题的一种方法,但是它没有广泛地提供类型相关的信息。类型特征是一种用于收集类型信息的技术。在它的帮助下,我们可以做出更智能的决策,在泛型编程中开发高质量的优化算法。
在本节中,我们将介绍如何实现类型特征,然后向您展示如何使用类型信息来优化算法。
为了理解类型特征,我们将看看boost::is_void
和boost::is_pointer
的经典实现。
首先,我们来看一个最简单的特质类,is_void
特质,由 boost 创建。它定义了一个用于实现默认行为的通用模板;也就是说,接受一个空类型,但其他任何东西都是空的。因此,我们有is_void::value = false
:
//primary class template is_void
template< typename T >
struct is_void{
static const bool value = false; //default value=false
};
然后,我们将其完全专门化为空型:
//"<>" means a full specialization of template class is_void
template<>
struct is_void< void >{ //fully specialization for void
static const bool value = true; //only true for void type
};
因此,我们有一个完整的性状类型,可以用来检测是否有任何给定的类型,T
,is_void
通过检查以下表达式:
is_void<T>::value
接下来,让我们学习如何在boost::is_pointer
性状中使用部分特化。
类似于boost::avoid
特征,一个主类模板定义如下:
//primary class template is_pointer
template< typename T >
struct is_pointer{
static const bool value = false;
};
然后,它部分地专用于所有指针类型:
//"typename T" in "<>" means partial specialization
template< typename T >
struct is_pointer< T* >{ //<T*> means partial specialization only for type T*
static const bool value = true; //set value as true
};
现在,我们有了一个完整的性状类型,可以通过检查以下表达式来检测是否有任何给定的类型,T
、is_pointer
:
is_pointer<T>::value
由于增强类型特征特性已经被正式引入到 C++ 11 标准库中,我们可以在下面的例子中显示std::is_void
和std::is_pointer
的用法,而不包括前面的源代码:
//ch4_15_traits_boost.cpp
#include <iostream>
#include <type_traits> //since C++ 11
using namespace std;
struct X {};
int main()
{
cout << boolalpha; //set the boolalpha format flag for str stream.
cout << is_void<void>::value << endl; //true
cout << is_void<int>::value << endl; //false
cout << is_pointer<X *>::value << endl; //true
cout << is_pointer<X>::value << endl; //false
cout << is_pointer<X &>::value << endl; //false
cout << is_pointer<int *>::value << endl; //true
cout << is_pointer<int **>::value << endl; //true
cout << is_pointer<int[10]>::value << endl; //false
cout << is_pointer< nullptr_t>::value << endl; //false
}
前面的代码在开头为字符串流设置了boolalpha
格式标志。通过这样做,所有的布尔值都是通过它们的文本表示来提取的,该文本表示为真或假。然后,我们用几个std::cout
打印is_void<T>::value
和is_pointer<T>::value
T4 的数值。每个值的输出显示在相应的注释行的末尾。
我们将使用一个经典的优化副本示例来展示类型特征的用法,而不是用一般的抽象方式来讨论这个主题。考虑称为copy
的标准库算法:
template<typename It1, typename It2>
It2 copy(It1 first, It1 last, It2 out);
显然,我们可以为任何迭代器类型编写通用版本的copy()
,也就是这里的It1
和It2
。然而,正如 boost 库的作者所解释的,在某些情况下,复制操作可以由memcpy()
执行。如果满足以下所有条件,我们可以使用memcpy()
:
- 两种类型的迭代器
It1
和It2
都是指针。 It1
和It2
必须指向相同的类型,除了 const 和 volatile 限定符It1
指向的类型必须提供一个简单的赋值运算符。
这里,简单赋值运算符意味着该类型要么是标量类型,要么是以下类型之一:
- 该类型没有用户定义的赋值运算符。
- 该类型中没有数据成员的引用类型。
- 必须在所有基类和数据成员对象中定义简单赋值运算符。
这里,标量类型包括算术类型、枚举类型、指针、指向成员的指针,或者这些类型之一的常量或易失性限定版本。
现在,让我们看看最初的实现。包括复印机类模板和用户界面功能两部分,即copy()
:
namespace detail{
//1\. Declare primary class template with a static function template
template <bool b>
struct copier {
template<typename I1, typename I2>
static I2 do_copy(I1 first, I1 last, I2 out);
};
//2\. Implementation of the static function template
template <bool b>
template<typename I1, typename I2>
I2 copier<b>::do_copy(I1 first, I1 last, I2 out) {
while(first != last) {
*out = *first;
++ out;
++ first;
}
return out;
};
//3\. a full specialization of the primary function template
template <>
struct copier<true> {
template<typename I1, typename I2>
static I2* do_copy(I1* first, I1* last, I2* out){
memcpy(out, first, (last-first)*sizeof(I2));
return out+(last-first);
}
};
} //end namespace detail
正如注释行中提到的,前面的复印机类模板有两个静态函数模板——一个是主要的,另一个是完全专门化的。主节点进行逐元素硬拷贝,而完全专门化节点通过memcpy()
一次拷贝所有元素:
//copy() user interface
template<typename I1, typename I2>
inline I2 copy(I1 first, I1 last, I2 out) {
typedef typename boost::remove_cv
<typename std::iterator_traits<I1>::value_type>::type v1_t;
typedef typename boost::remove_cv
<typename std::iterator_traits<I2>::value_type>::type v2_t;
enum{ can_opt = boost::is_same<v1_t, v2_t>::value
&& boost::is_pointer<I1>::value
&& boost::is_pointer<I2>::value
&& boost::has_trivial_assign<v1_t>::value
};
//if can_opt= true, using memcpy() to copy whole block by one
//call(optimized); otherwise, using assignment operator to
//do item-by-item copy
return detail::copier<can_opt>::do_copy(first, last, out);
}
为了优化复制操作,前面的用户界面功能定义了两个remove_cv
模板对象,v1_t
和v2_t
,然后评估can_opt
是否为真。之后,调用do_copy()
模板函数。通过使用 boost 实用程序库中发布的测试代码(algo_opt_ examples.cpp
),我们可以看到使用优化的实现有了显著的改进;也就是说,复制 char 或 int 类型的数据可能会快 8 到 3 倍。
最后,让我们用以下要点来结束这一部分:
- 除了类型之外,特征还能提供额外的信息。它是通过模板专门化实现的。
- 按照惯例,特征总是作为结构实现的。用于实现特征的结构被称为特征类。
- 比雅尼·斯特劳斯特鲁普说,我们应该把一个特性看作一个小对象,它的主要目的是携带信息,供另一个对象或算法用来确定策略或实现细节。进一步 阅读上下文【4】
- 斯科特·迈耶斯还总结说,我们应该使用性状类来收集关于类型的信息进一步阅读上下文【5】。
- 特征可以帮助我们以有效/优化的方式实现通用算法。
接下来,我们将探索 C++ 中的模板元编程。
一种计算机程序能够将其他程序视为其数据的编程技术被称为元编程。这意味着一个程序可以被设计成读取、生成、分析或转换其他程序,甚至在运行时修改自己。元编程的一种是编译器,它把一个文本格式的程序作为输入语言(C、Fortran、Java 等),用输出语言产生另一个二进制机器码格式的程序。
C++ 模板元编程 ( TMP )意味着使用模板在 C++ 中生成元程序。它有两个组件——必须定义一个模板,并且必须实例化一个已定义的模板。TMP 是图灵完备的,这意味着它有能力计算任何可计算的东西,至少在原则上是这样。另外,因为变量在 TMP 中都是不可变的(变量是常数),所以递归而不是迭代被用来处理集合的元素。
为什么我们需要 TMP?因为它可以在执行时间内加速我们的程序!但是由于优化世界中没有免费的午餐,我们为 TMP 支付的价格是更长的编译时间和/或更大的二进制代码大小。另外,不是每个问题都可以用 TMP 解决;它只在我们计算编译时不变的东西时有效;例如,找出所有小于常数整数的主数,常数整数的阶乘,展开常数循环或迭代,等等。
从实用的角度来看,模板元编程具有解决以下三类问题的能力:编译时计算、编译时优化,以及通过避免运行时的虚拟表查找,用静态多态替换动态多态。在下面的小节中,我们将提供每个类别的示例来演示元编程是如何工作的。
通常,如果任务的输入和输出在编译时是已知的,我们可以使用模板元编程在编译期间进行计算,从而节省任何运行时开销和内存占用。这在实时高 CPU 利用率项目中非常有用。
我们来看看阶乘函数,它计算*n*!
。这是所有小于或等于 *n、*的正整数与 0 的乘积!=1。由于递归的概念,我们可以使用一个简单的函数来实现它,如下所示:
//ch4_17_factorial_recursion.cpp
#include <iostream>
uint32_t f1(const uint32_t n) {
return (n<=1) ? 1 : n * f1(n - 1);
}
constexpr uint32_t f2(const uint32_t n) {
return ( n<=1 )? 1 : n * f2(n - 1);
}
int main() {
uint32_t a1 = f1(10); //run-time computation
uint32_t a2 = f2(10); //run-time computation
const uint32_t a3 = f2(10); //compile-time computation
std::cout << "a1=" << a1 << ", a2=" << a2 << std::endl;
}
f1()
在运行时进行计算,而f2()
可以在运行时或编译时进行计算,这取决于它的用法。
同样,通过使用具有非类型参数、其专门化和递归概念的模板,该问题的模板元编程版本如下:
//ch4_18_factorial_metaprogramming.cpp
#include <iostream>
//define a primary template with non-type parameters
template <uint32_t n>
struct fact {
***const static uint32_t*** value = n * fact<n - 1>::value;
//use next line if your compiler does not support declare and initialize
//a constant static int type member inside the class declaration
//enum { value = n * fact<n - 1>::value };
};
//fully specialized template for n as 0
template <>
struct fact<0> {
const static uint32_t value = 1;
//enum { value = 1 };
};
using namespace std;
int main() {
cout << "fact<0>=" << fact<0>::value << endl; //fact<0>=1
cout << "fact<10>=" << fact<10>::value << endl; //fact<10>=3628800
//Lab: uncomments the following two lines, build and run
// this program, what are you expecting?
//uint32_t m=5;
//std::cout << fact<m>::value << std::endl;
}
这里,我们创建了一个带有非类型参数的类模板,像其他常量表达式一样,const static uint32_t
或枚举常量的值在编译时进行计算。这个编译时评估约束意味着只有常量变量才有意义。此外,由于我们只使用类,静态对象是有意义的。
当编译器看到模板的新参数时,它会创建模板的新实例。例如,当编译器看到fact<10>::value
并试图创建一个参数为 10 的fact
的实例时,结果发现fact<9>
也必须被创建。对于fact<9>
,需要fact<8>
等等。最后编译器使用fact<0>::value
(为 1),编译时的递归终止。这个过程可以在下面的代码块中看到:
fact<10>::value = 10* fact<9>::value;
fact<10>::value = 10* 9 * fact<8>::value;
fact<10>::value = 10* 9 * 8 * fact<7>::value;
.
.
.
fact<10>::value = 10* 9 * 8 *7*6*5*4*3*2*fact<1>::value;
fact<10>::value = 10* 9 * 8 *7*6*5*4*3*2*1*fact<0>::value;
...
fact<10>::value = 10* 9 * 8 *7*6*5*4*3*2*1*1;
请注意,为了能够以这种方式使用模板,我们必须在模板参数列表中提供一个常量参数。这就是为什么如果取消最后两行代码的注释,会收到编译器的抱怨:fact:template parameter n: m: a variable with non-static storage duration cannot be used as a non-type argument
。
最后,让我们通过简单比较 constexpr 函数 ( CF )和 TMP 来结束这一小节:
- 计算时间 : CF 在编译时或者运行时执行,这取决于它的用法,但是 TMP 只在编译时执行。
- 参数列表 : CF 只能取值,但是 TMP 可以同时取值和类型参数。
- 控制结构 : CF 可以使用递归、条件、循环,但是 TMP 只使用递归。
虽然前面的例子可以在编译时计算一个常数整数的阶乘,但是我们可以使用一个运行时循环来展开两个- n 向量的点积(其中 n 在编译时是已知的)。更传统的长度- n 向量的好处是展开循环是可行的,这导致非常优化的代码。
例如,传统的点积函数模板可以通过以下方式实现:
//ch4_19_loop_unoolling_traditional.cpp
#include <iostream>
using namespace std;
template<typename T>
T dotp(int n, const T* a, const T* b)
{
T ret = 0;
for (int i = 0; i < n; ++ i) {
ret += a[i] * b[i];
}
return ret;
}
int main()
{
float a[5] = { 1, 2, 3, 4, 5 };
float b[5] = { 6, 7, 8, 9, 10 };
cout<<"dot_product(5,a,b)=" << dotp<float>(5, a, b) << '\n'; //130
cout<<"dot_product(5,a,a)=" << dotp<float>(5, a, a) << '\n'; //55
}
循环展开意味着如果我们可以将dotp()
函数中的 for 循环优化为a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] + a[4]*b[4]
,那么将节省更多的运行时计算。这正是元编程在下面的代码块中所做的:
//ch4_20_loop_unroolling_metaprogramming.cpp
#include <iostream>
//primary template declaration
template <int N, typename T>
class dotp {
public:
static T result(T* a, T* b) {
return (*a) * (*b) + dotp<N - 1, T>::result(a + 1, b + 1);
}
};
//partial specialization for end condition
template <typename T>
class dotp<1, T> {
public:
static T result(T* a, T* b) {
return (*a) * (*b);
}
};
int main()
{
float a[5] = { 1, 2, 3, 4, 5 };
float b[5] = { 6, 7, 8, 9, 10 };
std::cout << "dot_product(5,a,b) = "
<< dotp<5, float>::result( a, b) << '\n'; //130
std::cout << "dot_product(5,a,a) = "
<< dotp<5,float>::result( a, a) << '\n'; //55
}
类似于阶乘元编程示例,在dotp<5, float>::result( a, b)
语句中,实例化过程递归地执行以下计算:
dotp<5, float>::result( a, b)
= *a * *b + dotp<4,float>::result(a+1,b+1)
= *a * *b + *(a+1) * *(b+1) + dotp<3,float>::result(a+2,b+2)
= *a * *b + *(a+1) * *(b+1) + *(a+2) * *(b+2)
+ dotp<2,float>::result(a+3,b+3)
= *a * *b + *(a+1) * *(b+1) + *(a+2) * *(b+2) + *(a+3) * *(b+3)
+ dotp<1,float>::result(a+4,b+4)
= *a * *b + *(a+1) * *(b+1) + *(a+2) * *(b+2) + *(a+3) * *(b+3)
+ *(a+4) * *(b+4)
由于 N 为 5,所以递归调用dotp<n
、float>::results()
模板函数四次,直到到达dotp<1
、float>::results()
。由dotp<5
、float>::result( a, b)
评估的最终表达式显示在前一个块的最后两行。
多态性意味着多个函数具有相同的名称。动态多态性允许用户在运行时确定要执行的实际函数方法(参见第 3 章、面向对象编程的细节,更多细节*)、而静态*多态性意味着要调用的实际函数(或者一般来说,要运行的实际代码)在编译时是已知的。默认情况下,C++ 通过检查参数的类型和/或数量,在编译时将函数调用与正确的函数定义相匹配。这个过程也叫静态绑定或重载 *。*但是,通过使用虚函数,编译器在运行时也进行动态绑定或重写。
例如,在下面的代码中,虚函数alg()
在基函数class B
和派生函数class D
中定义。当我们使用派生对象指针p
作为基类的实例指针时,p->alg()
函数调用将调用在派生类中定义的派生alg()
:
//ch4_21_polymorphism_traditional.cpp
#include <iostream>
class B{
public:
B() = default;
virtual void alg() {
std::cout << "alg() in B";
}
};
class D : public B{
public:
D() = default;
virtual void alg(){
std::cout << "alg() in D";
}
};
int main()
{
//derived object pointer p as an instance pointer of the base class
B *p = new D();
p->alg(); //outputs "alg() in D"
delete p;
return 0;
}
但是,在多态行为不变并且可以在编译时确定的情况下,可以使用奇怪重复的模板模式 ( CRTP )来实现静态多态,它模仿静态多态并在编译时解析绑定。因此,程序在运行时将不再检查virtual-lookup-table
。下面的代码以静态多态性的方式实现了前面的示例:
//ch4_22_polymorphism_metaprogramming.cpp
#include <iostream>
template <class D> struct B {
void ui() {
static_cast<D*>(this)->alg();
}
};
struct D : B<D> {
void alg() {
cout << "D::alg()" << endl;
}
};
int main(){
B<D> b;
b.ui();
return 0;
}
总之,模板元编程的一般思想是让编译器在编译时做一些计算。这样,可以在一定程度上解决运行时开销。我们之所以能在编译时计算一些东西,是因为在运行时之前有些东西是不变的。
正如在进一步阅读上下文[14]中提到的,C++ TMP 是一种在编译时执行计算任务的非常强大的方法。第一种方法并不容易,我们必须非常小心编译错误,因为模板树是展开的。从实用的角度来看,boost 元编程库 ( MPL )是一个很好的入门参考。它以通用的方式为算法、序列和元功能提供了编译时 TMP 框架。此外,C++ 17 中新的std::variant
和std::visit
特性也可以用于静态多态,用于没有相关类型共享接口继承类型的场景。
在本章中,我们讨论了 C++ 中与泛型编程相关的主题。从回顾 C 宏和函数重载开始,我们介绍了 C++ 模板的开发动机。然后,我们展示了带有固定数量参数的类和函数模板的语法,以及它们的专门化和实例化。自 C++ 11 以来,变量模板被标准泛型函数和类模板所接受。基于此,我们进一步将模板参数和参数分为三类:非类型模板参数/参数、类型模板参数/参数和模板模板参数/参数。
我们还学习了特性和模板元编程。作为模板专门化的副产品,特性类可以为我们提供更多关于类型的信息。在类型信息的帮助下,最终,实现泛型算法的优化成为可能。类和/或函数模板的另一个应用是通过递归在编译时计算一些常量任务,这被称为模板元编程。它能够执行编译时计算和/或优化,并避免在运行时进行虚拟表查找。
现在,您应该对模板有了深刻的理解。您应该能够在应用中创建自己的函数和类模板,并练习使用特性来优化您的算法,以及使用模板元编程来进行编译时计算以进行额外的优化
在下一章中,我们将学习与内存和管理相关的主题,例如内存访问的概念、分配和取消分配技术,以及垃圾收集基础知识。这是 C++ 最独特的特性,因此每个 C++ 开发人员都必须理解。
- 宏的副作用是什么?
- 什么是类/函数模板?什么是模板类/函数?
- 什么是模板参数表?什么是模板参数列表?一旦我们有了一个类模板,我们就可以显式或隐式地实例化它。在什么样的场景下显式实例化是必要的?
- C++ 中多态是什么意思?函数重载和函数重写有什么区别?
- 什么是类型特征?我们如何实现类型特征?
- 在
ch4_5_class_template_implicit_inst_B.cpp
文件中,我们说隐式实例化生成X<int>
类,然后创建xi
对象并生成X<int>::f()
函数,但不生成X<int>::g()
。如何验证X<int>::g()
没有生成? - 使用模板元编程,解决 f(x,n) = x^n 的问题,其中 n 是常量, x 是变量。
- 扩展到 n=10,100,10^3,10^4,10^6 的价值观,...,直到达到系统内存限制。比较编译时间、目标文件大小和运行 CPU 时间。
如本章所述,查看以下来源,了解本章内容的更多信息:
-
莫里斯·米尔纳(1975 年)。*具有自反和多态类型的可计算函数的逻辑。*证明和改进程序会议记录。
-
*柯蒂斯,多萝西(2009-11-06)。CLU 主页。*计算机科学与人工智能实验室编程方法论组。麻省理工学院。
-
国际标准化组织发布的 Ada 2012 技术勘误表。阿达资源协会。2016-01-29.
-
https://www.adaic.org/2016/01/technical-corrigendum-for-ada-2012-published-by-iso/
-
B.斯特鲁普、 C++。
-
https://dl . ACM . org/doi/10.5555/1074100.1074189
-
S .迈耶斯,有效的 C++ 55 改进程序和设计的具体方法(第三版),第 7 章。
-
https://www . oreilly . com/library/view/effect-c-55/0321334876/
-
D. Gregor and J. Järvi (February 2008). *Variadic Templates for C++ 0x.*Journal of Object Technology. pp. 31–51
-
https://www.boost.org/为型性状,单位测验等。
-
https://www . IBM . com/support/knowledge center/ssw _ IBM _ I _ 72/rzarg/templates . htm获取通用模板讨论。
-
代码分析工具 https://stack overflow . com/questions/546669/c-code-analysis-tool。
-
https://en.cppreference.com为模板显式实例化。
-
http://www.cplusplus.com为图书馆参考和使用实例。
-
https://en.wikipedia.org/wiki/Template_metaprogramming为模板元编程。
-
K.生成编程:方法、工具和应用,第 10 章。
-
名词(noun 的缩写)乔舒蒂斯;D. Gregor 和 D. Vandevoorde, C++ 模板:完整指南(第二版),Addison-Wesley Professional 2017。*