第十三章:类型转换运算符
类型转换是一种机制,让程序员能够暂时或永久性改变编译器对对象的解释。注意,这并不意味着程序员改变了对象本身,而只是改变了对对象的解释。可改变对象解释方式的运算符称为类型转换运算符。
为何需要类型转换
如果 C++应用程序都编写得很完善,其处于类型是安全的且是强类型的世界,则没有必要进行类型转换,也不需要类型转换运算符。然而,在现实世界中,不同模块往往由不同的人编写,而使用不同开发环境的厂商需要协作。因此,程序员经常需要让编译器按其所需的方式解释数据,让应用程序能够成功编译并正确执行。
在 C++的发展过程中,不断有新的 C++类型转换运算符出现,这导致 C++编程社区分裂成两个阵营:一个阵营继续在其 C++应用程序中使用 C 风格类型转换;另一个阵营转而使用 C++编译器引入的类型转换关键字。前一个阵营认为,C++类型转换难以使用,且有时候功能变化不大,只有理论意义。后一个阵营则显然由 C++语法纯粹论者组成,他们通过指出 C 风格类型转换的缺陷以支持其论点。在现实世界中,这两个观点各行其道,读者最好通过阅读本章以了解每种风格的优缺点,然后形成自己的见解。
为何有些 C++程序员不喜欢 C 风格类型转换
当前,C++编译器仍需向后兼容,以确保遗留代码能够通过编译,因此支持下面这样的语法:
char* staticStr = "Hello World!";
int* intArray = (int*)staticStr; // Cast one problem away, create another
这种 C 风格类型转换实际上强迫编译器根据程序员的选择来解释目标对象。对不希望类型转换破坏其倡导的类型安全的 C++程序员来说,这是无法接受的。
C++类型转换运算符
虽然类型转换有缺点,但也不能抛弃类型转换的概念。在很多情况下,类型转换是合理的需求,可解决重要的兼容性问题。C++提供了一种新的类型转换运算符,专门用于基于继承的情形,这种情形在 C 语言编程中并不存在。
4 个 C++类型转换运算符如下:
• static_cast
• dynamic_cast
• reinterpret_cast
• const_cast
这 4 个类型转换运算符的使用语法相同:
destination_type result = cast_operator<destination_type> (object_to_cast);
使用 static_cast
static_cast 用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换—这种转换原本将自动或隐式地进行。用于指针时,static_cast 实现了基本的编译阶段检查,确保指针被转换为相关类型。
这改进了 C 风格类型转换,在 C 语言中,可将指向一个对象的指针转换为完全不相关的类型,而编译器不会报错。使用 static_cast 可将指针向上转换为基类类型,也可向下转换为派生类型,如下面的示例代码所示:
Base* objBase = new Derived();
Derived* objDer = static_cast<Derived*>(objBase); //OK
//类 Unrelated 和 Base 父类没有关系,因此下面的转换报错
Unrelated* notRelated = static_cast<Unrelated*> (objBase); //Error
//static_cast 不允许转换成不相干的类型
然而,static_cast 只验证指针类型是否相关,而不会执行任何运行阶段检查。因此,程序员可使用static_cast 编写如下代码,而编译器不会报错:
Base* objBase = new Base();
Derived* objDer = static_cast<Derived*>(objBase); //仍然是错误的
其中 objDer 实际上指向一个不完整的 Derived 对象,因为它指向的对象实际上是 Base()类型。
由于 static_cast 只在编译阶段检查转换类型是否相关,而不执行运行阶段检查,因此 objDer -> DerivedFunction()能够通过编译,但在运行阶段可能导致意外结果。
除用于向上转换和向下转换外,static_cast 还可在很多情况下将隐式类型转换为显式类型,以引起程序员或代码阅读者的注意:
double Pi = 3.14159265;
int num = static_cast<int>(Pi); // Making an otherwise implicit cast, explicit
在上述代码中,使用 num = Pi 将获得同样的效果,但使用 static_cast 可让代码阅读者注意到这里使用了类型转换,并指出(对知道 static_cast 的人而言)编译器根据编译阶段可用的信息进行了必要的调整,以便执行所需的类型转换。对于使用关键字 explicit 声明的转换运算符和构造函数,要使用它们,也必须通过 static_cast。
使用 dynamic_cast 和运行阶段类型识别
顾名思义,与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。可检查 dynamic_cast 操作的结果,以判断类型转换是否成功。使用 dynamic_cast 运算符的典型语法如下:
destination_type* Dest = dynamic_cast<class_type*>(Source);
if(Dest) // Check for success of the casting operation
Dest->CallFunc ();
例如:
Base* objBase = new Derived();
//演示 dynamic_cast
Derived* objDer = dynamic_cast<Derived*>(objBase);
if(objDer) //查看转换是否成功
objDer->CallDerivedFunction();
如上述代码所示,给定一个指向基类对象的指针,程序员可使用 dynamic_cast 进行类型转换,并在使用指针前检查指针指向的目标对象的类型。在上述示例代码中,目标对象的类型显然是 Derived,因此这些代码只有演示价值。然而,情况并非总是如此,例如,将 Derived*传递给接受 Base*参数的函数时。该函数可使用 dynamic_cast 判断基类指针指向的对象的类型,再执行该类型特有的操作。总之,可使用 dynamic_cast 在运行阶段判断类型,并在安全时使用转换后的指针。下面的程序使用了一个您熟悉的继承层次结构—Tuna 和 Carp 类从基类 Fish 派生而来,其中的函数 DetectFishtype( )动态地检查 Fish 指针指向的对象是否是 Tuna 或 Carp。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Fish {
public:
virtual void Swim() {
cout << "Fish swims in water" << endl;
}
//基类总是应该保证有一个虚析构函数
virtual ~Fish(){}
};
class Tuna : public Fish{
public:
void Swim() {
cout << "Tuna swims real fast in the sea" << endl;
}
void BecomeDinner() {
cout << "Tuna became dinner in Sushi" << endl;
}
};
class Carp : public Fish {
public:
void Swim() {
cout << "Carp swims real slow in the lake" << endl;
}
void Talk() {
cout << "Carp talked Carp" << endl;
}
};
void DetectFishType(Fish* objFish) {
//确保是 Tuna 类型才执行后面的事情
Tuna* objTuna = dynamic_cast<Tuna*>(objFish);
if (objTuna) { //检查是否转换成功
cout << "Detected Tuna. Making Tuna dinner: " << endl;
objTuna->BecomeDinner();
}
Carp* objCarp = dynamic_cast<Carp*>(objFish);
if (objCarp) {
cout << "Detected Carp. Making carp talk: " << endl;
objCarp->Talk();
}
cout << "Verifying type using virtual Fish::Swim: " << endl;
objFish->Swim(); // calling virtual function Swim
}
int main() {
Carp myLunch;
Tuna myDinner;
DetectFishType(&myDinner);
cout << endl;
DetectFishType(&myLunch);
return 0;
}
输出:
这个示例的独特之处在于,给定一个基类指针(Fish*),您可动态地检测它指向的是否是 Tuna 或 Carp。
这种动态检测(运行阶段类型识别)是在第 38~54 行定义的函数 DetectFishType( )中进行的。在第 40 行,使用 dynamic_cast 传入的基类指针(Fish*)参数指向的是否是 Tuna 对象。如果该 Fish*指向的是 Tuna 对象,该运算符将返回一个有效的地址,否则将返回 NULL。因此,总是需要检查 dynamic_cast 的结果是否有效。如果通过了第 40 行的检查,您便知道指针 objTuna 指向的是一个有效的 Tuna 对象,因此可以使用它来调用函数 Tuna::BecomeDinner( ),如第 43 行所示。如果传入的 Fish*参数指向的是 Carp 对象,则使用它来调用函数 Carp::Talk( ),如第 49 行所示。返回之前,DetectFishType( )调用了 Swim( ),以验证对象类型;Swim( )是一个虚函数,这行代码将根据指针指向的对象类型,调用相应类(Tuna 或 Carp)中实现的方法 Swim( )。
使用 reinterpret_cast
reinterpret_cast 是 C++中与 C 风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种,不管它们是否相关;也就是说,它使用如下所示的语法强制重新解释类型:
Base* objBase = new Base ();
Unrelated* notRelated = reinterpret_cast<Unrelated*>(objBase);
// The code above compiles, but is not good programming!
这种类型转换实际上是强制编译器接受 static_cast 通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换为 API(应用程序编程接口)能够接受的简单类型。
由于其他 C++类型转换运算符都不允许执行这种有悖类型安全的转换,因此除非万不得已,否则不要使用 reinterpret_cast 来执行不安全(不可移植)的转换。
使用 const_cast
const_cast 让程序员能够关闭对象的访问修饰符 const。
您可能会问:为何要进行这种转换?在理想情况下,程序员将经常在正确的地方使用关键字 const。不幸的是,现实世界并非如此,像下面这样的代码随处可见:
class SomeClass {
public:
// ...
void DisplayMembers(); //problem - display function isn't const
};
在下面的函数中,以 const 引用的方式传递 object 显然是正确的。毕竟,显示函数应该是只读的,不应调用非 const 成员函数,即不应调用能够修改对象状态的函数。然而,DisplayMembers()本应为 const 的,但却没有这样定义。如果 SomeClass 归您所有,且源代码受您控制,则可对 DisplayMembers()进行修改。然而,在很多情况下,它可能属于第三方库,无法对其进行修改。在这种情况下,const_cast 将是您的救星。
void DisplayAllData (const SomeClass& object) {
object.DisplayMembers (); // Compile failure
// reason: call to a non-const member using a const reference
}
在这种情况下,调用 DisplayMembers()的语法如下:
void DisplayAllData (const SomeClass& object) {
SomeClass& refData = const_cast<SomeClass&>(object);
refData.DisplayMembers(); // Allowed!
}
除非万不得已,否则不要使用 const_cast 来调用非 const 函数。一般而言,使用 const_cast 来修改 const 对象可能导致不可预料的行为。
另外,const_cast 也可用于指针:
void DisplayAllData (const SomeClass* data) {
// data->DisplayMembers(); Error: attempt to invoke a non-const function!
SomeClass* pCastedData = const_cast<SomeClass*>(data);
pCastedData->DisplayMembers(); // Allowed!
}
总结
本章介绍了各种 C++类型转换运算符以及支持和反对类型转换运算符的根据。一般而言,应避免使用类型转换。
第十四章:宏和模板简介
预处理器与编译器
顾名思义,预处理器在编译器之前运行,换句话说,预处理器根据程序员的指示,决定实际要编译的内容。预处理器编译指令都以 # 打头,例如:
#include <iostream>
#define ARRAY_LENGTH 25
上述代码演示了两种预处理器编译指令,一是使用 #define 定义常量,二是使用#define定义宏函数。这两个编译指令都告诉编译器,将每个宏实例(ARRAY_LENGTH 或 SQUARE)替换为其定义的值。
使用宏避免多次包含
C++程序员通常在 .H 文件(头文件)中声明类和函数,并在 .CPP 文件中定义函数,因此需要在 .CPP 文件中使用预处理器编译指令 #include <header> 来包含头文件。
如果在头文件 class1.h 中声明了一个类,而这个类将 class2.h 中声明的类作为其成员,则需要在 class1.h 中包含 class2.h。如果设计非常复杂,即第二个类需要第一个类,则在 class2.h 中也需要包含 class1.h!
然而,在预处理器看来,两个头文件彼此包含对方会导致递归问题。为了避免这种问题,可结合使用宏以及预处理器编译指令 #ifndef 和 #endif。
包含 <header2.h> 的 head1.h 类似于下面这样:
#ifndef HEADER1_H_//多重引用保护
#define HEADER1_H_ // 预处理器将一次性读取此行及其后续行
#include <header2.h>
class Class1 {
// class members
};
#endif // end of header1.h
header2.h 与此类似,但宏定义不同,且包含的是<header1.h>:
#ifndef HEADER2_H_//multiple inclusion guard
#define HEADER2_H_
#include <header1.h>
class Class2 {
// class members
};
#endif // end of header2.h
因此,预处理器首次处理 header1.h 并遇到 #ifndef 后,发现宏 HEADER1_H_还未定义,因此继续处理。#ifndef 后面的第一行定义了宏 HEADER1_H_,确保预处理器再次处理该文件时,将在遇到包含 #ifndef 的第一行时结束,因为其中的条件为 false。header2.h 与此类似。在 C++编程领域,这种简单的机制无疑是最常用的宏功能之一。
使用 #define 编写宏函数
预处理器对宏指定的文本进行简单替换,因此也可以使用宏来编写简单的函数,例如:
#define SQUARE(x) ((x) * (x))
宏函数通常用于执行非常简单的计算。相比于常规函数调用,宏函数的优点在于,它们将在编译前就地展开,因此在有些情况下有助于改善代码的性能。
为什么要使用括号
原因在于宏的计算方式——预处理器支持的文本替换机制。
在省略了括号的情况下,简单的文本替换破坏了编程逻辑!使用括号有助于避免这种问题。
使用 assert 宏验证表达式
编写程序后,立即单步执行以测试每条代码路径很不错,但对大型应用程序来说可能不现实。比较现实的做法是,插入检查语句,对表达式或变量的值进行验证。
assert 宏让您能够完成这项任务。要使用 assert 宏,需要包含<assert.h>,其语法如下:
assert (expression that evaluates to true or false);
下面是一个示例,它使用 assert( ) 来验证指针的值:
#include <assert.h>
int main() {
char* sayHello = new char [25];
assert(sayHello != NULL); // throws a message if pointer is NULL
// other code
delete [] sayHello;
return 0;
}
assert( )在指针无效时将指出这一点:
在 Microsoft Visual Studio 中,assert( ) 让您能够单击 Retry 按钮返回应用程序,而调用栈将指出哪行代码没有通过断言测试。这让 assert( )成为一项方便的调试功能。例如,可使用 assert 对函数的输入参数进行验证。长期而言,assert 有助于改善代码的质量,强烈推荐使用它。
使用宏函数的优点和缺点
宏函数将在编译前就地展开,因此简单宏的性能优于常规函数调用。这是因为函数调用要求创建调用栈、传递参数等,这些开销占用的 CPU时间通常比 MIN 执行的计算还多。
然而,宏不支持任何形式的类型安全,这是一个严重的缺点。另外,复杂的宏调试起来也不容易。如果需要编写独立于类型的泛型函数,又要确保类型安全,可使用模板函数,而不是宏函数。
这将在下一节介绍。另外如果要改善性能,可将函数声明为内联的。
模板简介
模板无疑是 C++语言中最强大却最少被使用的特性之一。
在 C++ 中,模板让程序员能够定义一种适用于不同类型对象的行为。这听起来有点像宏(参见前面用于判断两个数中哪个更大的简单宏 MAX),但宏不是类型安全的,而模板是类型安全的。
模板声明语法
模板声明以关键字 template 打头,接下来是类型参数列表。这种声明的格式如下:
template <参数列表>
template function / class declaration
关键字 template 标志着模板声明的开始,接下来是模板参数列表。该参数列表包含关键字 typename,它定义了模板参数 objType,objType 是一个占位符,针对对象实例化模板时,将使用对象的类型替换它。
template<typename T1, typename T2 = T1>
bool TemplateFunction(const T1& param1, const T2& param2);
// A template class
template <typename T1, typename T2 = T1>
class MyTemplate{
private:
T1 member1;
T2 member2;
public:
T1 GetObj1(){ return member1; }
// ... other members
};
上述代码演示了一个模板函数和一个模板类,它们都接受两个模板参数:T1 和 T2,其中 T2 的类型默认为 T1。
各种类型的模板声明
模板函数
假设要编写一个函数,它适用于不同类型的参数,为此可使用模板语法!
template <typename objType>
const objType& GetMax(const objType& value1, const objType& value2){
if(value1 > value2)
return value1;
else
return value2;
}
下面是一个使用该模板的示例:
int num1 = 25;
int num2 = 40;
int maxVal = GetMax<int>(num1, num2);
double double1 = 1.1;
double double2 = 1.001;
double maxVal = GetMax<double>(double1, double2)
注意到调用 GetMax 时使用了,这将模板参数 objType 指定为 int。上述代码将导致编译器生成模板函数 GetMax 的两个版本,如下所示:
// 版本1
const int& GetMax(const int& value1, const int& value2) {
//...
}
//版本2
const double& GetMax(const double& value1, const double& value2) {
// ...
}
然而,实际上调用模板函数时并非一定要指定类型,因此下面的函数调用没有任何问题:
int maxVal = GetMax(num1, num2);
在这种情况下,编译器很聪明,知道这是针对整型调用模板函数。如下面示例代码所示:
#include <iostream>
#include <string>
using namespace std;
template<typename Type>
const Type& GetMax(const Type& value1, const Type& value2) {
if (value1 > value2)
return value1;
else {
return value2;
}
}
template<typename Type>
void DisplayComparison(const Type& value1, const Type& value2) {
cout << "GetMax(" << value1 << ", " << value2 << ") = ";
cout << GetMax(value1, value2) << endl;
}
int main() {
int num1 = -101, num2 = 2011;
DisplayComparison(num1, num2);
double d1 = 3.14, d2 = 3.1416;
DisplayComparison(d1, d2);
string name1("Jack"), name2("John");
DisplayComparison(name1, name2);
return 0;
}
在 main( )函数中的代码表明,可将同一个模板函数用于不同类型的数据:int、double 和 std::string。模板函数不仅可以重用(就像宏函数一样),而且更容易编写和维护,还是类型安全的。
请注意,调用 DisplayComparison 时,也可显式地指定类型,如下所示:
DisplayComparison<int>(num1, num2);
然而,调用模板函数时没有必要这样做。
您无需指定模板参数的类型,因为编译器能够自动推断出类型;但使用模板类时,需要这样做。
模板类
模板类是模板化的 C++ 类,是蓝图的蓝图。使用模板类时,可指定要为哪种类型具体化类。
这让您能够创建不同的 Human 对象,即有的年龄存储在 long long 成员中,有的存储在 int 成员中,还有的存储在 short 成员中。
下面是一个简单的模板类,它只有单个模板参数 T,用于存储一个成员变量:
template<typename T>
class HoldVarTypeT{
private:
T value;
public:
void setValue(const T& newValue){
value = newValue;
}
T& GetValue(){ return value; }
};
类 HoldVarTypeT 用于保存一个类型为 T 的变量,该变量的类型是在使用模板时指定的。下面来看该模板类的一种用法:
HoldVarTypeT<int> holdInt; //template instantiation for int
holdInt.SetValue(5);
cout << "The value stored is :" holdInt.GetValue() << endl;
这里使用该模板类来存储和检索类型为 int 的对象,即使用 int 类型的模板参数实例化 Template 类。同样,这个类也可以用于处理字符串,其用法类似:
HoldVarTypeT <char*> holdStr;
holdStr.SetValue("Sample string");
cout << "The value stored is: " << holdStr.GetValue() << endl;
因此,这个模板类定义了一种模式,并可针对不同的数据类型实现这种模式。
声明包含多个参数的模板
模板参数列表包含多个参数,参数之间用逗号分隔。因此,如果要声明一个泛型类用于存储两个类型可能不同的对象,可以使用如下所示的代码(这个模板类包含两个模板参数):
template<typename value1, typename value2>
class HoldsPair{
private:
T1 value1;
T2 value2;
};
在这里,类 HoldsPair 接受两个模板参数,参数名分别为 T1 和 T2。可使用这个类来存储两个类型相同或不同的对象,如下所示:
// A template instantiation that pairs an int with a double
HoldsPair <int, double> pairIntDouble (6, 1.99);
// A template instantiation that pairs an int with an int
HoldsPair <int, int> pairIntDouble (6, 500);
声明包含默认参数的模板
可以修改前面的 HoldsPair <…>,将模板参数的默认类型指定为 int:
template<typename T1=int, typename T2=int>
class HoldsPair{
//...
};
这与给函数指定默认参数值极其类似,只是这里指定的是默认类型。
这样,前述第二种 HoldsPair 用法可以简写为:
// Pair an int with an int (default type)
HoldsPair <> pairInts (6, 500);
一个模板示例
下面使用前面讨论的 HoldsPair 模板来进行开发:
#include <iostream>
#include <string>
using namespace std;
template<typename T1 = int,typename T2=double>
class HoldsPair {
private:
T1 value1;
T2 value2;
public:
HoldsPair(const T1& val1, const T2& val2)
:value1(val1), value2(val2) {}
const T1& GetFirstValue()const {
return value1;
}
const T2& GetSecondValue() const {
return value2;
}
};
int main() {
HoldsPair<> pairIntDbl(300, 10.09);
HoldsPair<short, const char*> pairShortStr(25, "Learn templates, love C++");
cout << "The first object contains -" << endl;
cout << "Value 1 : " << pairIntDbl.GetFirstValue() << endl;
cout << "Value 2 : " << pairIntDbl.GetSecondValue() << endl;
cout << "The second object contains -" << endl;
cout << "Value 1 : " << pairShortStr.GetFirstValue() << endl;
cout << "Value 2 : " << pairShortStr.GetSecondValue() << endl;
return 0;
}
HoldsPair 定义了一种模式,可通过重用该模式针对不同的变量类型实现相同的逻辑。因此,使用模板可提高代码的可复用性。
模板的实例化和具体化
模板类是创建类的蓝图,因此在编译器看来,仅当模板类以某种方式被使用后,其代码才存在。换言之,对于您定义了但未使用的模板类,编译器将忽略它。然而,当您像下面这样通过提供模板参数来实例化模板类(如 HoldsPair)时:
HoldsPair<int, double> pairIntDbl;
就相当于命令编译器使用模板来创建一个类,即使用模板参数指定的类型(这里是 int 和 double)实例化模板。因此,对模板来说,实例化指的是使用一个或多个模板参数来创建特定的类型。
另一方面,在有些情况下,使用特定的类型实例化模板时,需要显式地指定不同的行为。这就是具体化模板,即为特定的类型指定行为。下面是模板类 HoldsPair 的一个具体化,其中两个模板参数的类型都为 int:
template<> class HoldsPair<int, int> {
// implementation code here
};
不用说,具体化模板的代码必须在模板定义后面。
下面的程序是一个模板具体化示例,演示了使用同一个模板可创建不同的具体化版本。
#include <iostream>
#include <string>
using namespace std;
template<typename T1 = int,typename T2=double>
class HoldsPair {
private:
T1 value1;
T2 value2;
public:
HoldsPair(const T1& val1, const T2& val2)
:value1(val1), value2(val2) {}
const T1& GetFirstValue() const;
const T2& GetSecondValue() const;
};
template<> class HoldsPair<int, int> {
private:
int value1;
int value2;
string strFun;
public:
HoldsPair(const int& val1, const int& val2) // constructor
: value1(val1), value2(val2) {}
const int& GetFirstValue() const{
cout << "Returning integer " << value1 << endl;
return value1;
}
};
int main() {
HoldsPair<int, int> pairIntInt(222, 333);
pairIntInt.GetFirstValue();
return 0;
}
对比两次的模板类 HoldsPair 的行为,将发现它们的行为有天壤之别。
事实上,上述代码中的这个模板定义甚至都没有提供存取函数 GetFirstValue() 和 GetSecondValue() 的实现,但程序依然能够通过编译。这是因为编译器只需考虑针对<int, int>的模板实例化,而在这个实例化中,我们提供了完备的具体实现。总之,这个示例不仅演示了模板具体化,还表明根据模板的使用情况,编译器可能忽略模板代码。
模板类和静态成员
前面说过,在编译器看来,仅当模板被使用时,其代码才存在。在模板类中,静态成员属性的工作原理是什么样的呢?
第 9 章介绍过,如果将类成员声明为静态的,该成员将由类的所有实例共享。模板类的静态成员与此类似,由特定具体化的所有实例共享。也就是说,如果模板类包含静态成员,该成员将在针对 int 具体化的所有实例之间共享;同样,它还将在针对 double 具体化的所有实例之间共享,且与针对 int 具体化的实例无关。
#include <iostream>
#include <string>
using namespace std;
template<typename T1>
class TestStatic {
public:
static int staticVal;
};
//静态成员初始化
template<typename T>
int TestStatic<T>::staticVal;
int main() {
TestStatic<int> intInstance;
cout << "Setting staticVal for intInstance to 2011" << endl;
intInstance.staticVal = 2011;
TestStatic<double> dblnstance;
cout << "Setting staticVal for Double_2 to 1011" << endl;
dblnstance.staticVal = 1011;
cout << "intInstance.staticVal = " << intInstance.staticVal << endl;
cout << "dblnstance.staticVal = " << dblnstance.staticVal << endl;
return 0;
}
输出表明,编译器在两个不同的静态成员中存储了两个不同的值,但这两个静态成员都名为 staticVal。也就是说,对于针对每种类型具体化的类,编译器确保其静态变量不受其他类的影响。
参数数量可变的模板
假定您要编写一个将两个值相加的通用函数,为此可编写下面这样的模板函数 Sum():
template <typename T1, typename T2, typename T3>
void Sum(T1& result, T2 num1, T3 num2)
{
result = num1 + num2;
return;
}
这很简单。然而,如果需要编写一个函数,能够计算任意数量值的和,就需要使用参数数量可变的模板。参数数量可变的模板是 2014 年发布的 C++14 新增的,下面的程序演示了如何使用参数数量可变的模板来定义刚才说的函数。
#include <iostream>
#include <string>
using namespace std;
template < typename Res, typename ValType>
void Sum(Res & result, ValType & val){
result = result + val;
}
template < typename Res, typename First, typename... Rest>
void Sum(Res& result, First val1, Rest... valN)
{
result = result + val1;
return Sum(result, valN ...);
}
int main() {
double dResult = 0;
Sum(dResult, 3.14, 4.56, 1.1111);
cout << "dResult = " << dResult << endl;
string strResult;
Sum(strResult, "Hello ", "World");
cout << "strResult = " << strResult.c_str() << endl;
return 0;
}
使用参数数量可变的模板定义的函数 Sum()不仅能够处理不同类型的参数,还能够处理不同数量的参数。
编译期间,编译器将根据调用 Sum() 的情况创建正确的代码,并反复处理提供的参数,直到将所有的参数都处理完毕。
参数数量可变的模板是 C++新增的一项强大功能,可用于执行数学运算,也可用于完成某些简单的任务。通过使用参数数量可变的模板,程序员可避免反复实现执行任务的各种重载版本,从而创建出更简短、更容易维护的代码。
通过支持参数数量可变的模板,C++还打开了支持元组的大门。std::tuple 就是实现元组的模板类,您可使用任意数量的元素来实例化这个模板类,其中每个元素都可为任何类型。要访问这些元素,可使用标准库函数 std::get。如下面程序所示:
#include <iostream>
#include <string>
#include <tuple>
using namespace std;
template<typename tupleType>
void DisplayTupleInfo(tupleType& tup) {
const int numMembers = tuple_size<tupleType>::value;
cout << "Num elements in tuple: " << numMembers << endl;
cout << "Last element value: " << get<numMembers - 1>(tup) << endl;
}
int main() {
tuple<int, char, string> tup1(make_tuple(101, 's', "Hello Tuple!"));
DisplayTupleInfo(tup1);
auto tup2(make_tuple(3.14, false));
DisplayTupleInfo(tup2);
auto concatTup(tuple_cat(tup2, tup1)); // contains tup2, tup1 members
DisplayTupleInfo(concatTup);
double pi;
string sentence;
tie(pi, ignore, ignore, ignore, sentence) = concatTup;
cout << "Unpacked! Pi: " << pi << " and \"" << sentence << "\"" << endl;
return 0;
}
元组是一个高级概念,常用于通用模板编程。这里提到这个主题旨在让您对元组有大致的了解,因为它还在不断发展变化中。
使用 static_assert 执行编译阶段检查
static_assert 是 C++11 新增的一项功能,让您能够在不满足指定条件时禁止编译。这好像不可思议,但对模板类来说很有用。例如,您可能想禁止针对 int 实例化模板类,为此可使用 static_assert,它是一种编译阶段断言,可用于在开发环境(或控制台中)显示一条自定义消息:
static_assert(expression being validated, "Error message when check fails");
要禁止针对类型 int 实例化模板类,可使用 static_assert( ),并将 sizeof(T)与 sizeof(int)进行比较,如果它们相等,就显示一条错误消息:
static_assert(sizeof(T) != sizeof(int), "No int please!");
下面代码演示了一个模板类,它使用 static_assert( ) 禁止针对特定类型进行实例化。
#include <iostream>
using namespace std;
template<typename T>
class EverythingButInt {
public:
EverythingButInt() {
static_assert(sizeof(T) != sizeof(int), "No int please!");
}
};
int main() {
EverythingButInt<int> test;
return 0;
}
没有输出,因为这个程序不能通过编译,它显示一条错误消息,指出您指定的类型不正确。
在实际 C++ 编程中使用模板
模板一个重要而最强大的应用是在标准模板库(STL)中。STL 由一系列模板类和函数组成,它们分别包含泛型实用类和算法。这些 STL 模板类让您能够实现动态数组、链表以及包含键-值对的容器,而 sort 等算法可用于这些容器,从而对容器包含的数据进行处理。
前面介绍的模板语法有助于读者使用本书后面将详细介绍的 STL 容器和函数;更深入地理解 STL将有助于使用 STL 中经过测试的可靠实现,从而编写出更高效的 C++程序,还有助于避免在模板细节上浪费时间。
总结
本章更详细地介绍了预处理器。每当您运行编译器时,预处理器都将首先运行,对#define 等指令进行转换。
预处理器执行文本替换,但在使用宏时替换将比较复杂。通过使用宏函数,可根据在编译阶段传递给宏的参数进行复杂的文本替换。将宏中的每个参数放在括号内以确保进行正确的替换,这很重要。
模板有助于编写可重用的代码,它向开发人员提供了一种可用于不同数据类型的模式。模板可以取代宏,且是类型安全的。学习本章介绍的模板知识后,便为学习如何使用 STL 做好了准备!