本章介绍了与此编译器相关的语言扩展。在命令行上指定某些编译器选项之后,编译器才能识别本章中描述的某些功能。相关编译器选项在相应章节中列出。
使用 -features=extensions 选项可以编译其他 C++ 编译器通常接受的非标准代码。必须编译无效代码且不允许修改代码而使之有效时,您可以使用该选项。
本章介绍了使用 -features=extensions 选项时编译器支持的语言扩展。
可以很容易的将每个支持无效代码的实例转变为所有编译器接受的有效代码。如果允许使代码有效,那么您应该使代码有效而不是使用该选项。使用 -features=extensions 选项可以使某些编译器拒绝的无效代码永远存在。
可使用下列声明说明符来协助约束外部符号的声明和定义。文件链接到共享库或可执行文件之前,静态归档或目标文件指定的作用域限制不会生效。尽管如此,编译器仍然可以执行显示链接程序作用域说明符的某些优化。
通过使用这些说明符,您不必再使用链接程序作用域的 mapfile。也可以通过在命令行上指定-xldscope 来控制变量作用域的缺省设置。
有关更多信息,请参见A.2.136 -xldscope={v}。
表 4–1 链接程序作用域声明说明符
符号定义可以用更多限制的说明符来重新声明,但是不可以用较少限制的说明符重新声明。符号定义后,不可以用不同的说明符声明符号。
__global 是限制最少的作用域,__symbolic 是限制较多的作用域,而 __hidden 是限制最多的作用域。
因为虚函数的声明影响虚拟表的结构和解释,所以所有虚函数对包括类定义的所有编译单元必须是可视的。
可以将链接程序作用域说明符应用于结构、类和联合声明和定义中,因为 C++ 类可能要求生成隐式信息,如虚拟表和运行时类型信息。在这种情况下,说明符后跟结构、类或联合关键字。这种应用程序为其所有隐式成员隐含了相同的链接程序作用域。
为了在动态库方面与 Microsoft Visual C++ (MSVC++) 中的相似作用域功能兼容,也支持以下语法:
__declspec(dllexport) 等效于 __symbolic |
__declspec(dllimport) 等效于 __global |
在 Sun C++ 中使用此语法时,应将选项 -xldscope=hidden 添加到 CC 命令行。结果与使用 MSVC++ 得到的结果相当。在 MSVC++ 中,__declspec(dllimport) 应当仅用于外部符号的声明,而不用于定义。示例:
__declspec(dllimport) int foo(); // OK __declspec(dllimport) int bar() { ... } // not OK |
MSVC++ 中,对于将 dllimport 用于定义没有严格规定,而使用 Sun C++ 时结果则不同。尤其是,使用 Sun C++ 时将 dllimport 用于定义得到的是具有全局链接的符号而不是符号链接。Microsoft Windows 上的动态库不支持符号的全局链接。如果遇到此问题,可以更改源代码,对定义使用 dllexport 而不是 dllimport。这样,使用 MSVC++ 和使用 Sun C++ 得到的结果相同。
通过声明线程局部变量,可以利用线程局部存储。线程局部变量声明普通变量声明与声明说明符 __thread 组成。有关更多信息,请参见A.2.182 -xthreadvar[= o]。
必须将 __thread 说明符包括在第一个线程变量声明中。使用 __thread 说明符声明的变量的绑定方式与没有 __thread 说明符时相同。
只能使用 __thread 说明符声明静态持续时间的变量。具有静态持续时间的变量包括了文件全局、文件静态、函数局部静态和类静态成员。不能使用 __thread 说明符声明动态或自动持续时间的变量。线程变量可以具有静态初始化函数,但是不可以具有动态初始化函数或析构函数。例如,允许 __thread int x = 4;,但不允许 __thread int x = f();。线程变量不能包含具有重要构造函数和析构函数的类型。具体来说,就是线程变量的类型不能为 std::string。
运行时对线程变量的地址运算符 (&) 求值并返回当前线程变量的地址。因此,线程变量的地址不是常量。
线程变量的地址在相应线程的生命周期中是稳定的。进程中任何线程都可以在线程变量的生命周期任意使用该变量的地址。不能在线程终止后使用线程变量的地址。线程变量的所有地址在线程终止后都是无效的。
C++ 标准规定,覆盖虚拟函数在异常中允许的限制不得低于它覆盖的任何函数的限制。该虚函数可能与覆盖的任何函数具有相同或更多的限制。注意,不存在异常规范也允许任何异常。
例如,假定通过指向基类的指针调用函数。如果函数具有异常规范,则可以计算出没有其他正抛出的异常。如果覆盖函数具有限制较少的规范,则不可预料的异常可能会被抛出,这会导致奇怪的程序行为并且终止程序。这就是规则的原因。
使用 -features=extensions 时,编译器允许覆盖异常规范限制较小的函数。
使用 -features=extensions 时,编译器允许对 enum 类型和变量进行前向声明。此外,编译器允许声明不完整 enum 类型的变量。编译器总是假定不完整 enum 类型的大小和范围与当前平台上的 int 类型相同。
以下是两行无效代码示例,如果使用 -features=extensions 选项,可对其进行编译。
enum E; // invalid: forward declaration of enum not allowed E e; // invalid: type E is incomplete |
因为 enum 定义不能互相引用,并且 enum 定义不能交叉引用另一种类型,所以从来不必对枚举类型进行前向声明。要使代码有效,可以总是先提供 enum 的完整定义,然后再使用它。
在 64 位体系结构上,enum 要求的大小可能比 int 类型大。如果是这种情况,并且如果向前声明和定义在同一编译中是可视的,那么编译器将发出错误。如果实际大小不是假定的大小并且编译器没有发现这个差异,那么代码将编译并链接,但有可能不能正常运行。可能出现奇怪的程序行为,尤其是 8 字节值存储在 4 字节变量中时。
使用 -features=extensions 时,不完整的 enum 类型以前向声明处理。例如,以下是无效代码,如果使用 -features=extensions 选项,可对其进行编译。
typedef enum E F; // invalid, E is incomplete |
如前所述,可以总是先包括 enum 类型的定义,然后再使用。
因为 enum 声明并不引入作用域,所以 enum 名称不能作为作用域限定符来使用。例如,以下代码是无效的。
enum E {e1, e2, e3}; int i = E::e1; // invalid: E is not a scope name |
要编译该无效代码,请使用 -features=extensions 选项。-features=extensions 选项指示编译器在作用域限定符是 enum 类型的名称的情况下忽略该作用域限定符。
要使代码有效,请删除无效的限定符 E::。
使用该选项提高了排字错误的可能性,产生了编译没有错误消息的错误程序。
匿名结构声明是既不声明结构标记也不声明对象或 typedef 名称的声明。C++ 中不允许匿名结构。
-features=extensions 选项允许使用匿名 struct 声明,但仅作为联合的成员。
以下代码是无效匿名 struct 声明示例,如果使用 -features=extensions 选项,可对其进行编译。
union U { struct { int a; double b; }; // invalid: anonymous struct struct { char* c; unsigned d; }; // invalid: anonymous struct }; |
struct 成员的名称是可视的,没有 struct 成员名称的限定。如果该代码示例中提供了 U 的定义,则可以编写:
U u; u.a = 1; |
匿名结构与匿名联合服从相同的限制。
请注意,可以通过为每个 struct 提供一个名称以使代码有效,如:
union U { struct { int a; double b; } A; struct { char* c; unsigned d; } B; }; U u; U.A.a = 1; |
不允许获取临时变量的地址。例如,因为以下代码获取了构造函数调用创建的变量地址,所以这些代码是无效的。但是,如果使用 -features=extensions 选项,编译器将接受该无效代码。
class C { public: C(int); ... }; void f1(C*); int main() { f1(&C(2)); // invalid } |
注意,可以通过使用显式变量来使该代码有效。
C c(2); f1(&c); |
函数返回时,临时对象被销毁。程序员应确保临时变量的地址没有留下。此外,销毁临时变量(例如 f1)时,临时变量中存储的数据会丢失。
下面的代码是无效的:
class A { friend static void foo(<args>); ... }; |
因为类名具有外部链接并且所有定义必须是相等的,所以友元函数也必须具有外部链接。但是,如果使用 -features=extensions 选项,编译器将接受该代码。
程序员处理该无效代码的方法大概是在类 A 的实现文件中提供非成员 "helper" 函数。可以通过使 foo 成为静态成员函数得到相同效果。如果不要客户端调用函数,则可以使该函数私有化。
如果使用该扩展,则任何客户端都可以“劫取”您的类。任何客户端都可以包括类的头文件,然后定义其自身的静态函数 foo,该函数将自动成为类的友元。结果就好像是您使类的所有成员成为了公共的。
使用 -features=extensions 时,编译器将每个函数中的标识符 __func__ 隐式声明为静态 const char 数组。如果程序使用标识符,编译器还会提供以下定义,其中,function-name 是函数原始名称。类成员关系、名称空间和重载不反映在名称中。
static const char __func__[] = "function-name"; |
例如,请考虑以下代码段。
#include <stdio.h> void myfunc(void) { printf("%s\n", __func__); } |
每次调用函数时,函数将把以下内容打印到标准输出流。
myfunc |
此属性附加于 struct 或 union 类型定义中,它指定结构或联合的每个成员(除了零宽度位字段)的放置,以最大限度地减小所需内存。附加于 enum 定义时,此属性指示应使用最小的整数类型。
为 struct 和 union 类型指定此属性等效于对每个结构或联合成员指定 packed 属性。
在以下示例中,struct my_packed_struct 的成员紧紧打包在一起,但其成员的内部布局并不打包。为此,还需要打包 struct my_unpacked_struct。
struct my_unpacked_struct { char c; int i; ; struct __attribute__ ((__packed__)) my_packed_struct { char c; int i; struct my_unpacked_struct s; }; |
只能对 enum、struct 或 union 的定义指定此属性,不能对未定义枚举类型、结构或联合的 typedef 指定此属性。
C++ 程序的文件组织需要比典型的 C 程序更加小心。本章说明了如何建立头文件和模板定义。
创建有效的头文件是很困难的。头文件通常必须适应 C 和 C++ 的不同版本。要提供模板,请确保头文件能容纳多个包含(幂等)。
可能需要开发能够包含在 C 和 C++ 程序中的头文件。但是,称为“传统 C”的 Kernighan 和 Ritchie C (K&C)、ANSI C、 Annotated Reference Manual C++ (ARM C++) 以及 ISO C++ 有时要求一个头文件中同一个程序元素有不同的声明或定义。(有关语言和版本之间的变化的其他信息,请参见《C++ 迁移指南》。) 要使头文件符合所有这些标准,可能需要根据预处理程序宏 __STDC__ 和 __cplusplus 的存在情况或值来使用条件编译。
在 K&R C 中没有定义宏 __STDC__,但在 ANSI C 和 C++ 中都对其进行了定义。可使用该宏将 K&R C 代码与 ANSI C 或 C++ 代码区分开。该宏最适用于从非原型函数定义中区分原型函数定义。
#ifdef __STDC__ int function(char*,...); // C++ & ANSI C declaration #else int function(); // K&R C #endif |
在 C 中没有定义宏 __cplusplus,但在 C++ 中对其进行了定义。
早期版本的 C++ 定义了宏 c_plusplus,但没有定义 cplusplus。现在已不再定义宏 c_plusplus。
可使用 __cplusplus 宏的定义来区分 C 和 C++。该宏在保证为函数声明指定 extern "C" 接口时非常有用,如以下示例所示。为了防止出现 extern "C" 指定不一致,切勿将 #include 指令放在 extern "C" 链接指定的作用域中。
#include “header.h” ... // ... other include files... #if defined(__cplusplus) extern “C” { #endif int g1(); int g2(); int g3() #if defined(__cplusplus) } #endif |
在 ARM C++ 中,__cplusplus 宏的值为 1。在 ISO C++ 中,该宏的值为 199711L(用 long 常量表示的标准年月)。使用这个宏的值区分 ARM C++ 和 ISO C++。这个宏值在保护模板语法的更改时极为有用。
// template function specialization #if __cplusplus < 199711L int power(int,int); // ARM C++ #else template <> int power(int,int); // ISO C++ #endif |
头文件应当是幂等的。也就是说,多次包括头文件的效果和仅包括一次的效果完全相同。该特性对于模板尤其重要。通过设置预处理程序条件以防止头文件体多次出现,可以很好的实现幂等。
#ifndef HEADER_H #define HEADER_H /* contents of header file */ #endif |
可以用两种方法组织模板定义: 使用包括的定义和使用独立的定义。包括的定义组织允许对模板编译进行更多的控制。
在将模板的声明和定义放在使用该模板的文件中时,组织是包括定义的组织。例如:
main.cc
template <class Number> Number twice(Number original); template <class Number> Number twice(Number original ) { return original + original; } int main() { return twice<int>(-3); } |
使用模板的文件包括了包含模板声明和模板定义的文件时,使用模板的该文件的组织也是包括定义的组织。例如:
twice.h
#ifndef TWICE_H #define TWICE_H template <class Number> Number twice(Number original); template <class Number> Number twice( Number original ) { return original + original; } #endif |
main.cc
#include “twice.h” int main() { return twice(-3); } |
使模板头文件幂等是非常重要的。(请参见5.1.2 幂等头文件。)
另一种组织模板定义的方法是将定义保留在模板定义文件中,如以下示例所示。
twice.h
#ifndef TWICE_H #define TWICE_H template <class Number> Number twice(Number original); #endif TWICE_H |
twice.cc
template <class Number> Number twice( Number original ) { return original + original; } |
main.cc
#include “twice.h” int main( ) { return twice<int>( -3 ); } |
模板定义文件不得包括任何非幂等头文件,而且通常根本不需要包括任何头文件。(请参见5.1.2 幂等头文件。)请注意,并非所有编译器都支持模板的独立定义模型。
一个单独的定义文件作为头文件时,该文件可能会被隐式包括在许多文件中。因此,它不应该包含任何函数或变量定义(除非这些定义是模板定义的一部分)。一个单独的定义文件可以包含类型定义,包括 typedef。
尽管通常会使用模板定义文件的源文件扩展名(即 .c、.C、.cc、.cpp、.cxx 或 .c++),但模板定义文件是头文件。如果需要,编译器会自动包括它们。模板定义文件不应单独编译。
如果将模板声明放置在一个文件中,而将模板定义放置在另一个文件中,则必须仔细考虑如何构造定义文件,如何命名定义文件和如何放置定义文件。此外也需要向编译器显式指定定义的位置。有关模板定义搜索规则的信息,请参阅7.5 模板定义搜索。
有了模板,就可以采用类型安全方法来编写适用于多种类型的单一代码。本章介绍了模板的概念和函数模板上下文中的术语,讨论了更复杂的(更强大的)类模板,描述了模板的组成,此外还讨论了模板实例化、缺省模板参数和模板专门化。本章的结尾部分讨论了模板的潜在问题。
函数模板描述了仅用参数或返回值的类型来区分的一组相关函数。
使用模板之前,请先声明。以下示例中的声明提供了使用模板所需的足够信息,但没有提供实现模板所需的足够信息。
template <class Number> Number twice( Number original ); |
在此示例中,Number 是模板参数,它指定模板描述的函数范围。更具体地说,Number 是模板类型参数,在模板定义中使用它表示确定的模板使用位置处的类型。
如果要声明模板,请先定义该模板。定义提供了实现模板所需的足够信息。以下示例定义了在前一个示例中声明的模板。
template <class Number> Number twice( Number original ) { return original + original; } |
因为模板定义通常出现在头文件中,所以模板定义必须在多个编译单元中重复。不过所有的定义都必须是相同的。该限制称为一次定义规则。
声明后,模板可以像其他函数一样使用。它们的使用由命名模板和提供函数参数组成。编译器可以从函数参数类型推断出模板类型参数。例如,您可以使用上面声明的模板,具体步骤如下所示。
double twicedouble( double item ) { return twice( item ); } |
如果模板参数不能从函数参数类型推断出,则调用函数时必须提供模板参数。例如:
template<class T> T func(); // no function arguments int k = func<int>(); // template argument supplied explicitly |
类模板描述了一组相关的类或数据类型,它们只能通过类型来区分:整数值、指向(或引用)具有全局链接的变量的指针、其他的组合。类模板尤其适用于描述通用但类型安全的数据结构。
类模板声明仅提供了类的名称和类的模板参数。此类声明是不完整的类模板。
以下示例是名为 Array 类的模板声明,该类可接受任何类型作为参数。
template <class Elem> class Array; |
该模板用于名为 String 的类,该类接受 unsigned int 作为参数。
template <unsigned Size> class String; |
类模板定义必须声明类数据和函数成员,如以下示例所示。
template <class Elem> class Array { Elem* data; int size; public: Array( int sz ); int GetSize(); Elem& operator[]( int idx ); }; |
template <unsigned Size> class String { char data[Size]; static int overflows; public: String( char *initial ); int length(); }; |
与函数模板不同,类模板可以同时有类型参数(如 class Elem)和表达式参数(如 unsigned Size)。表达式参数可以是:
具有整型或枚举的值
指向对象的指针或到对象的引用
指向函数的指针或到函数的引用
指向类成员函数的指针
类模板的完整定义需要类模板函数成员和静态数据成员的定义。动态(非静态)数据成员由类模板声明完全定义。
模板函数成员的定义由模板参数专门化后跟函数定义组成。函数标识符通过类模板的类名称和模板参数限定。以下示例说明了 Array 类模板的两个函数成员的定义,该模板中指定了模板参数 template <class Elem>。每个函数标识符都通过模板类名称和模板参数 Array<Elem> 限定。
template <class Elem> Array<Elem>::Array( int sz ) {size = sz; data = new Elem[size];} template <class Elem> int Array<Elem>::GetSize() { return size; } |
该示例说明了 String 类模板的函数成员的定义。
#include <string.h> template <unsigned Size> int String<Size>::length( ) {int len = 0; while (len < Size && data[len]!= ’\0’) len++; return len;} template <unsigned Size> String<Size>::String(char *initial) {strncpy(data, initial, Size); if (length( ) == Size) overflows++;} |
模板静态数据成员的定义由后跟变量定义的模板参数专门化组成,在此处变量标识符通过类模板名称和类模板实元参数来限定。
template <unsigned Size> int String<Size>::overflows = 0; |
模板类可以在使用类型的任何地方使用。指定模板类包括了提供模板名称和参数的值。以下示例中的声明根据 Array 模板创建 int_array 变量。变量的类声明及其一组方法类似于 Array 模板中的声明和方法,除了 Elem 替换为 int(请参6.3 模板实例化)。
Array<int> int_array(100); |
此示例中的声明使用 String 模板创建 short_string 变量。
String<8> short_string("hello"); |
需要任何其他成员函数时,您可以使用模板类成员函数。
int x = int_array.GetSize( ); |
int x = short_string.length( ); . |
模板实例化是生成采用特定模板参数组合的具体类或函数(实例)。例如,编译器生成一个采用 Array<int> 的类,另外生成一个采用 Array<double> 的类。通过用模板参数替换模板类定义中的模板参数,可以定义这些新的类。在前面“类模板”一节介绍的 Array<int> 示例中,编译器用 int 替换所有 Elem。
使用模板函数或模板类时需要实例。如果这种实例还不存在,则编译器隐式实例化模板参数组合的模板。
编译器仅为实际使用的那些模板参数组合而隐式实例化模板。该方法不适用于构造提供模板的库。C++ 提供了显式实例化模板的功能,如以下示例所示。
要显式实例化模板函数,请在 template 关键字后接函数的声明(不是定义),且函数标识符后接模板参数。
template float twice<float>(float original); |
在编译器可以推断出模板参数时,模板参数可以省略。
template int twice(int original); |
要显式实例化模板类,请在 template 关键字后接类的声明(不是定义),且在类标识符后接模板参数。
template class Array<char>; |
template class String<19>; |
显式实例化类时,所有的类成员也必须实例化。
要显式实例化模板类函数成员,请在 template 关键字后接函数的声明(不是定义),且在由模板类限定的函数标识符后接模板参数。
template int Array<char>::GetSize(); |
template int String<19>::length(); |
要显式实例化模板类静态数据成员,请在 template 关键字后接成员的声明(不是定义),且在由模板类限定的成员标识符后接模板参数。
template int String<19>::overflows; |
可以嵌套使用模板。这种方式尤其适用于在通用数据结构上定义通用函数,与在标准 C++ 库中相同。例如,模板排序函数可以通过一个模板数组类进行声明:
template <class Elem> void sort(Array<Elem>); |
并定义为:
template <class Elem> void sort(Array<Elem> store) {int num_elems = store.GetSize(); for (int i = 0; i < num_elems-1; i++) for (int j = i+1; j < num_elems; j++) if (store[j-1] > store[j]) {Elem temp = store[j]; store[j] = store[j-1]; store[j-1] = temp;}} |
上述示例定义了针对预先声明的 Array 类模板对象的排序函数。下一个示例说明了排序函数的实际用法。
Array<int> int_array(100); // construct an array of ints sort(int_array); // sort it |
您可以将缺省值赋予类模板(但不是函数模板)的模板参数。
template <class Elem = int> class Array; template <unsigned Size = 100> class String; |
如果模板参数具有缺省值,则该参数后的所有参数也必须具有缺省值。模板参数仅能具有一个缺省值。
将某些模板参数组合视为特殊的参数可以提高性能,如以下 twice 示例所示。而模板说明可能无法用于其一组可能的参数,如以下 sort 示例所示。模板专门化允许您定义实际模板参数给定组合的可选实现。模板专门化覆盖了缺省实例化。
使用模板参数的组合之前,您必须声明专门化。以下示例声明了twice 和 sort 的专用实现。
template <> unsigned twice<unsigned>( unsigned original ); |
template <> sort<char*>(Array<char*> store); |
如果编译器可以明确决定模板参数,则您可以省略模板参数。例如:
template <> unsigned twice(unsigned original); |
template <> sort(Array<char*> store); |
必须定义声明的所有模板专门化。下例定义了上一节中声明的函数。
template <> unsigned twice<unsigned>(unsigned original) {return original << 1;} |
#include <string.h> template <> void sort<char*>(Array<char*> store) {int num_elems = store.GetSize(); for (int i = 0; i < num_elems-1; i++) for (int j = i+1; j < num_elems; j++) if (strcmp(store[j-1], store[j]) > 0) {char *temp = store[j]; store[j] = store[j-1]; store[j-1] = temp;}} |
专门化与其他任何模板一样使用并实例化,除此以外,完全专用模板的定义也是实例化。
在前一个示例中,模板是完全专用的。也就是说,模板定义了特定模板参数的实现。模板也可以部分专用,这意味着只有某些模板参数被指定,或者一个或多个参数被限定到某种类型。生成的部分专门化仍然是模板。例如,以下代码样本说明了主模板和该模板的完全专门化。
template<class T, class U> class A {...}; //primary template template<> class A<int, double> {...}; //specialization |
以下代码说明了主模板部分专门化的示例。
template<class U> class A<int> {...}; // Example 1 template<class T, class U> class A<T*> {...}; // Example 2 template<class T> class A<T**, char> {...}; // Example 3 |
示例 1 提供了用于第一个模板参数是 int 类型的情况的特殊模板定义。
示例 2 提供了用于第一个模板参数是任何指针类型的情况的特殊模板定义。
示例 3 提供了用于第一个模板参数是任何类型的指针到指针而第二个模板参数是 char 类型的情况的特殊模板定义。
本节描述了使用模板时会遇到的问题。
有时模板定义使用模板参数或模板本身未定义的名称。如此,编译器解决了封闭模板作用域的名称,该模板可以在定义或实例化点的上下文中。名称可以在不同的位置具有不同的含义,产生不同的解析。
名称解析比较复杂。因此,您不应该依赖除一般全局环境中提供的名称外的非本地名称。也就是说,仅使用在任何地方都用相同方法声明和定义的非本地名称。在以下示例中,模板函数 converter 使用了非本地名称 intermediary 和 temporary。在 use1.cc 和 use2.cc 中这些名称的定义不同,因此在不同的编译器下可能会生成不同的结果。为了能可靠地使用模板,所有非本地名称(该示例中为 intermediary 和 temporary)在任何地方都必须有相同的定义。
use_common.h // Common template definition template <class Source, class Target> Target converter(Source source) {temporary = (intermediary)source; return (Target)temporary;} use1.cc typedef int intermediary; int temporary; #include "use_common.h" use2.cc typedef double intermediary; unsigned int temporary; #include "use_common.h" |
一个常见的非本地名称用法是在模板内使用 cin 和 cout 流。有时程序员要将流作为模板参数传递,这时就要引用到全局变量。但 cin 和 cout 在任何地方都必须有相同的定义。
模板实例化系统取决于类型名称,等效于决定哪些模板需要实例化或重新实例化。因此本地类型用作模板参数时,会导致严重的问题。小心在代码中也出现类似的问题。例如:
array.h template <class Type> class Array { Type* data; int size; public: Array(int sz); int GetSize(); }; array.cc template <class Type> Array<Type>::Array(int sz) {size = sz; data = new Type[size];} template <class Type> int Array<Type>::GetSize() {return size;} file1.cc #include "array.h" struct Foo {int data;}; Array<Foo> File1Data(10); file2.cc #include "array.h" struct Foo {double data;}; Array<Foo> File2Data(20); |
在 file1.cc 中记录的 Foo 类型与在 file2.cc 中记录的 Foo 类型不同。以这种方法使用本地类型会出现错误和意外的结果。
模板在使用之前必须先声明。模板的使用由友元声明构成,不是由模板的声明构成。实际的模板声明必须在友元声明之前。例如,编译系统尝试链接以下示例中生成的目标文件时,对未实例化的 operator<< 函数,会生成未定义错误。
array.h // generates undefined error for the operator<< function #ifndef ARRAY_H #define ARRAY_H #include <iosfwd> template<class T> class array { int size; public: array(); friend std::ostream& operator<<(std::ostream&, const array<T>&); }; #endif array.cc #include <stdlib.h> #include <iostream> template<class T> array<T>::array() {size = 1024;} template<class T> std::ostream& operator<<(std::ostream& out, const array<T>& rhs) {return out <<’[’ << rhs.size <<’]’;} main.cc #include <iostream> #include "array.h" int main() { std::cout << "creating an array of int... " << std::flush; array<int> foo; std::cout << "done\n"; std::cout << foo << std::endl; return 0; } |
请注意,因为编译器将以下代码作为普通函数(array 类的 friend)的声明进行读取,所以编译期间不会出现错误消息。
friend ostream& operator<<(ostream&, const array<T>&); |
因为 operator<< 实际上是模板函数,所以需要在声明 template class array 之前提供模板声明。但是,由于 operator<< 有一个 array<T> 类型的参数,因此必须在声明函数之前声明 array<T>。文件 array.h 必须如下所示:
#ifndef ARRAY_H #define ARRAY_H #include <iosfwd> // the next two lines declare operator<< as a template function template<class T> class array; template<class T> std::ostream& operator<<(std::ostream&, const array<T>&); template<class T> class array { int size; public: array(); friend std::ostream& operator<< <T> (std::ostream&, const array<T>&); }; #endif |
C++ 标准要求使用具有限定名的类型,这些限定名取决于要用 typename 关键字显式标注为类型名称的模板参数。即使编译器“知道”它应该是一个类型,也是如此。以下示例中的注释说明了具有要用 typename 关键字的限定名的类型。
struct simple { typedef int a_type; static int a_datum; }; int simple::a_datum = 0; // not a type template <class T> struct parametric { typedef T a_type; static T a_datum; }; template <class T> T parametric<T>::a_datum = 0; // not a type template <class T> struct example { static typename T::a_type variable1; // dependent static typename parametric<T>::a_type variable2; // dependent static simple::a_type variable3; // not dependent }; template <class T> typename T::a_type // dependent example<T>::variable1 = 0; // not a type template <class T> typename parametric<T>::a_type // dependent example<T>::variable2 = 0; // not a type template <class T> simple::a_type // not dependent example<T>::variable3 = 0; // not a type |
由于 ">>" 字符序列解释为右移运算符,因此在一个模板名称中使用另一个模板名称时必须小心。确保相邻的 ">" 字符之间至少有一个空格。
例如,以下是形式错误的语句:
Array<String<10>> short_string_array(100); // >> = right-shift |
被解释为:
Array<String<10 >> short_string_array(100); |
正确的语法为:
Array<String<10> > short_string_array(100); |
在模板定义中,编译器不支持引用在全局作用域或名称空间中声明为静态的对象或函数。如果生成了多个实例,则每个示例引用到了不同的对象,因此违背了单次定义规则(C++ 标准的 3.2 节)。通常的失败指示是链接时丢失符号。
如果想要所有模板实例化共享单一对象,那么请使对象成为已命名空间的非静态成员。如果想要模板类的每个实例化不同对象,那么请使对象成为模板类的静态成员。如果希望每个模板函数实例化的对象不同,请使对象成为函数的本地对象。
如果要通过指定 -instances=extern 生成多个程序或库,建议在不同的目录中生成这些程序或库。如果要在同一目录中生成多个程序,那么您需要清除不同生成程序之间的系统信息库。这样可以避免出现任何不可预料的错误。有关更多信息,请参见7.4.4 共享模板系统信息库。
考虑以下示例的 make 文件 a.cc、b.cc、x.h 和 x.cc。请注意,仅当指定了 -in tances=extern 时,此示例才有意义:
........ Makefile ........ CCC = CC all: a b a: $(CCC) -I. -instances=extern -c a.cc $(CCC) -instances=extern -o a a.o b: $(CCC) -I. -instances=extern -c b.cc $(CCC) -instances=extern -o b b.o clean: /bin/rm -rf SunWS_cache *.o a b |
... x.h ... template <class T> class X { public: int open(); int create(); static int variable; }; |
... x.cc ... template <class T> int X<T>::create() { return variable; } template <class T> int X<T>::open() { return variable; } template <class T> int X<T>::variable = 1; |
... a.cc ... #include "x.h" int main() { X<int> temp1; temp1.open(); temp1.create(); } |
... b.cc ... #include "x.h" int main() { X<int> temp1; temp1.create(); } |
如果同时生成了 a 和 b,请在两个生成之间添加 make clean。以下命令会引起错误:
example% make a example% make b |
以下命令不会产生任何错误:
example% make a example% make clean example% make b |
C++ 编译器在模板编译方面处理的工作要比传统 UNIX 编译器处理的工作多。C++ 编译器必须按需为模板实例生成目标代码。该编译器会使用模板系统信息库在多个独立的编译间共享模板实例,此外还接受某些模板编译选项。编译器必须在各个源文件中定位模板定义,并维护模板实例和主线代码之间的一致性。
如果使用标志 -verbose=template,C++ 编译器会在编译模板期间通知重要事件。但如果使用缺省值 -verbose=no%template,编译器不会发出通知。+w 选项可以在进行模板实例化时提供其他有关潜在问题的指示信息。
CCadmin(1) 命令管理模板系统信息库(只能与选项 -instances=extern 一起使用)。例如,程序中的更改会造成某些实例化过度,这样会浪费存储空间。CCadmin – clean 命令(以前是 ptclean)清除所有实例及关联数据。实例化仅在需要时才重新创建。
为了生成模板实例,编译器将内联模板函数看作内联函数。编译器像管理其他内联函数一样管理这些内联模板函数,另外本章中的说明不适用于模板内联函数。
编译器通常是分别实例化各个模板类成员,因此,编译器仅实例化程序中使用的成员。仅用于调试器的方法会因此而不正常地实例化。
有两种方法确保调试成员可用于调试器。
首先,编写使用模板类实例成员(否则无用)的非模板函数,不需要调用该函数。
其次,使用 -template=wholeclass 编译器选项,该选项指示编译器实例化模板类的所有非模板非内联成员(如果实例化这些相同成员中的任何一个)。
ISO C++ 标准允许开发人员编写模板类,因为并不是所有成员都可以使用模板参数。只要非法成员未被实例化,程序就仍然完好。ISO C++ 标准库使用了这种技术。但是,-template=wholeclass 选项会实例化所有成员,因此不能用于此类使用有问题的模板参数实例化的模板类。
实例化是 C++ 编译器从模板创建可用的函数或对象的过程。C++ 编译器使用了编译时实例化,在编译对模板的引用时强制进行实例化。
编译时实例化的优点是:
调试更加简单-错误消息在上下文中出现,从而让编译器完全回溯到引用点。
模板实例化始终保持最新。
包括链接阶段在内的总编译时间减少了。
如果源文件位于不同的目录或您使用了具有模板符号的库,则模板可以多次实例化。
缺省情况下,实例会进入特殊地址区域,链接程序会识别并丢弃重复项。您可以指示编译器使用五个实例放置和链接方法之一: 外部、静态、全局、显式和半显式。
本节讨论了五种实例放置和链接方法。6.3 模板实例化中提供了有关生成实例的其他信息。
对于外部实例方法,所有实例都放置在模板系统信息库中。编译器确保只有一个一致的模板实例存在;这些实例既不是未定义的也不是多重定义的。模板仅在需要时才重新实例化。对于非调试代码,所有目标文件(包括模板缓存中的任何目标文件)在使用 -instances=extern 时的大小总量小于在使用 -instances=global 时的大小总量。
模板实例接受系统信息库中的全局链接。实例是使用外部链接从当前编译单元引用的。
如果在不同的步骤中进行编译和链接,并且在编译步骤中指定了 -instance=extern,则还必须在链接步骤中指定该选项。
这种方法的缺点是更改程序或程序发生重大更改时必须清除缓存。高速缓存是并行编译的瓶颈,这与使用 dmake 时一样,因为每次只能有一个编译访问高速缓存。另外,每个目录内仅能生成一个程序。
决定缓存中是否存在有效的模板实例比直接在主目标文件中创建实例(如果需要,用完后可以丢弃)要花费更长的时间。
可使用 -instances=extern 选项指定外部链接。
因为实例存储在模板系统信息库中,所以必须使用 CC 命令将使用外部实例的 C++ 对象链接到程序中。
如果要创建包含了使用的所有模板实例的库,请结合使用 CC 命令与 — xar 选项。而不要使用 ar 命令。例如:
example% CC– xar -instances=extern– o libmain.a a.o b.o c.o |
有关更多信息,请参见表 15–3。
如果指定了 -instance=extern,请勿在同一目录中运行不同的编译器版本,以避免可能的高速缓存冲突。使用 -instances=extern 模板模型时,请注意:
请勿在同一目录中创建不相关的二进制文件。在同一目录中创建的所有二进制文件(.o、.a、.so、可执行程序)都应该相关,因为两个或两个以上目标文件中名称相同的所有对象、函数和类型的定义都相同。
在同一目录中同时运行多个编译是安全的,例如使用 dmake 时。与另外一个链接步骤同时运行任何编译或链接步骤是不安全的。“链接步骤”指创建库或可执行程序的任何操作。确保 makefile 中的依赖性不允许任何内容与链接步骤以并行方式运行。
-instances=static 选项已过时。没有任何理由再使用 -instances=static,因为 -instances=global 现在提供了静态的所有优点而没有其缺点。早期的编译器中提供了此选项来克服现已不存在的问题。
对于静态实例方法,所有实例都被放置在当前编译单元内。因此,模板在每个重新编译期间重新实例化;这些实例不保存到模板系统信息库。
这种方法的缺点是不遵循语言语义,并且会生成很大的对象和可执行文件。
实例接收静态链接。这些实例在当前编译单元外部是不可视的或不可用的。因此,模板可以在多个目标文件中具有相同的实例化。因为多重实例产生了不必要的大程序,所以对于不可能多重实例化模板的小程序可以使用静态链接。
静态实例的编译速度很快,因此这种方法也适用于修复并继续方式的调试。(请参见《使用 dbx 调试程序》。)
如果您的程序取决于多个编译单元间的共享模板实例(例如模板类或模板函数的静态数据成员),请勿使用静态实例方法。否则程序会工作不正常。
可使用 -instances=static 编译器选项指定静态实例链接。
与早期的编译器发行版不同,现在不必预防出现一个全局实例有多个副本。
这种方法的优点是通常由其他编译器接受的不正确源代码也能在这种模式中接受。特别的是,从模板实例内对静态变量的引用是不合法的,但通常是可以接受的。
这种方法的缺点是单个目标文件会很大,原因是多个文件中模板实例有多个副本。如果编译目标文件以便进行调试时,有些使用了 -g 选项,而有些没有使用该选项,则很难预测是获得链接到程序中模板实例的调试版本还是非调试版本。
模板实例接收全局链接。这些实例在当前编译单元外部是可视的和可用的。
可使用 -instances=global 选项(这是缺省值)指定全局实例。
在显式实例方法中,仅为显式实例化的模板生成实例。隐式实例化不能满足该要求。实例被放置在当前编译单元内。
这种方法的优点是拥有最少的模板编译和最小的对象大小。
缺点是您必须手动执行所有的实例化。
模板实例接收全局链接。这些实例在当前编译单元外部是可视的和可用的。链接程序识别并丢弃重复项目。
可使用 -instances=explicit 选项指定显式实例。
使用半显式实例方法时,仅为显式实例化或模板体内隐式实例化的模板生成实例。那些被显式创建实例所需要的实例将会自动生成。主线代码中隐式实例化不满足该要求。实例被放置在当前编译单元内。因此,模板在每个重新编译期间重新实例化;生成的实例接收全局链接,且不会被保存到模板系统信息库中。
可使用 -instances=semiexplicit 选项指定半显式实例。
模板系统信息库中存储需单独进行编译的模板实例,以便仅在需要时编译模板实例。模板系统信息库包含了使用外部实例方法时模板实例化所需的所有非源文件。系统信息库不用于其他种类的实例。
缺省情况下,模板系统信息库位于名为 SunWS_cache 的高速缓存目录中。
缓存目录包含在放置目标文件的目录中。可以通过设置环境变量 SUNWS_CACHE_NAME 更改高速缓存目录的名称。请注意,SUNWS_CACHE_NAME 变量值必须是目录名称,而不能是路径名。这是因为编译器自动将模板缓存目录放置到了目标文件目录下,因此编译器已经具有了路径。
编译器必须存储模板实例时,编译器将模板实例存储在对应于输出文件的模板系统信息库中。例如,以下命令行会将目标文件写入 ./sub/a.o 并将模板实例写入包含在 ./sub/SunWS_cache 中的系统信息库。如果缓存目录不存在,且编译器需要实例化模板,则编译器将创建目录。
example% CC -o sub/a.o a.cc |
编译器从对应于编译器读取的目标文件的模板系统信息库读取。即,以下命令行从 ./sub1/SunWS_cache 和 ./sub2/SunWS_cache 读取,必要时,向 ./SunWS_cache 写入。
example% CC sub1/a.o sub2/b.o |
系统信息库中的模板不得违反 ISO C++ 标准的一次定义规则。也就是说,使用所有的模板时模板必须具有相同的源。违反该规则会产生不可预料的行为。
确保不违反该规则的最简单和最保守的方法是在任何一个目录内仅生成一个程序或库。两个不相关的程序可以使用相同类型的名称或外部名称来表示不同的内容。如果程序共享模板系统信息库,则模板定义会出现冲突,会产生不可预料的结果。
如果指定了 -instances=extern,模板系统信息库管理器可确保系统信息库中实例的状态与源文件一致且是最新的。
例如,如果使用 – g 选项编译源文件(调试),也会使用 – g 编译来自数据库中的所需文件。
此外,模板系统信息库会跟踪编译中的更改。例如,如果设置了 — DDEBUG 标志来定义名称 DEBUG,数据库中会记录下该信息。如果在以后的编译中省略该标志,则编译器重新实例化设置依赖性的这些模板。
如果删除模板的源代码或停止使用模板,模板的实例会保留在缓存中。如果更改函数模板的签名,使用旧签名的实例会保留在缓存中。如果因为这些问题在编译时或链接时遇到了异常行为,请清除模板缓存并重新生成程序。
使用独立定义模板组织时,模板定义在当前编译单元不可用,编译器必须搜索该定义。本节描述了编译器如何找到定义。
定义搜索有些复杂,并且很容易出现错误。因此如果可能,您应该使用定义包括模板文件组织。这样有助于避免一起定义搜索。请参见5.2.1 包括的模板定义。
如果使用 -template=no%extdef 选项,编译器将不搜索单独的源文件。
如果没有随选项文件一起提供的特定方向,则编译器使用 Cfront 样式的方法来定位模板定义文件。此方法要求模板定义文件包含的基名与模板声明文件包含的基名相同。此方法也要求模板定义文件位于当前 include 路径中。例如,如果模板函数 foo() 位于 foo.h 中,匹配的模板定义文件应该命名为 foo.cc 或某些其他可识别的源文件扩展名(.C、.c、.cc、.cpp、.cxx 或 .c++)。模板定义文件必须位于常规的 include 目录之一中,或位于与其匹配的头文件所在目录中。
可以用另外一种方法替代用 –I 设置的常规搜索路径,即使用选项 –ptidirectory 指定模板定义文件的搜索目录。多个 -pti 标志定义多个搜索目录-即搜索路径。如果使用 -ptidirectory,则编译器在该路径查找模板定义文件并忽略 –I 标志。由于 –ptidirectory 标志会使源文件的搜索规则变得复杂,因此应使用 –I 选项而不是 –ptidirectory 选项。
有时,编译器会生成令人费解的警告或错误消息,因为它会查找您不打算编译的文件。此问题通常是由于某个文件(如 foo.h)包含模板声明,且隐式包含了另一个文件(如 foo.cc)。
如果头文件 foo.h 有模板声明,缺省情况下,编译器会搜索名为 foo 且具有 C++ 文件扩展名(.C、.c、.cc、.cpp、.cxx 或 .c++)的文件。如果找到这样的文件,编译器将自动把它包含进来。有关这些搜索的更多信息,请参见7.5 模板定义搜索。
如果有一个不打算这样处理的文件 foo.cc,有两种解决方法:
更改 .h 或 .cc 文件的名称,以消除名称匹配。
可以通过指定 -template=no%extdef 选项来禁用模板定义文件的自动搜索。然后必须在代码中显式包含所有模板定义,并且不能使用“独立定义”模型。
本章讨论了 C++ 编译器的异常处理实现。11.2 在多线程程序中使用异常中提供了附加信息。有关异常处理的更多信息,请参见由 Bjarne Stroustrup 编著的《The C++ Programming Language》第三版(Addison-Wesley 出版,1997)。
异常处理设计用于仅支持同步异常,例如数组范围检查。同步异常这一术语意味着异常只能源于 throw 表达式。
C++ 标准支持具有终止模型的同步异常处理。终止意味着一旦抛出异常,控制永远不会返回到抛出点。
异常处理没有设计用于直接处理诸如键盘中断等异步异常。不过,如果小心处理,在出现异步事件时也可以进行异常处理。例如,要用信号进行异常处理工作,您可以编写设置全局变量的信号处理程序,并创建另外一个例程来定期轮询该变量的值,当该变量值发生更改时抛出异常。不能从信号处理程序抛出异常。
有五个与异常有关的运行时错误消息:
没有异常处理程序
未预料到的异常抛出
异常只能在处理程序中重新抛出
在堆栈展开时,析构函数必须处理自身的异常
内存不足
运行时检测到错误时,错误消息会显示当前异常的类型和这五个错误消息之一。缺省情况下,会调用预定义的函数 terminate(),该函数又会调用 abort()。
编译器使用异常规范中提供的信息来优化代码生成。例如,禁止不抛出异常的函数表条目,而函数异常规范的运行时检查在任何可能的地方被消除。
如果知道程序中未使用异常,可以使用编译器选项 -features=no%except 抑制支持异常处理的代码的生成。该选项的使用可以稍微减小代码的大小,并能加快代码的执行速度。不过,用禁用的异常编译的文件链接到使用异常的文件时,在用禁用的异常编译的文件中的某些局部对象在发生异常时不会销毁。缺省情况下,编译器生成支持异常处理的代码。通常都要启用异常,只有时间和空间的开销是考虑的重要因素时才禁止异常。
因为 C++ 标准库 dynamic_cast 和缺省运算符 new 要求使用异常,所以在标准模式(缺省模式)下编译时不应禁用异常。
标准头文件 <exception> 提供了 C++ 标准中指定的类和异常相关函数。仅在标准模式(编译器缺省模式,或使用选项 -compat=5)下编译时才可访问该头文件。以下摘录的部分代码显示了 <exception> 头文件声明。
// standard header <exception> namespace std { class exception { exception() throw(); exception(const exception&) throw(); exception& operator=(const exception&) throw(); virtual ~exception() throw(); virtual const char* what() const throw(); }; class bad_exception: public exception {...}; // Unexpected exception handling typedef void (*unexpected_handler)(); unexpected_handler set_unexpected(unexpected_handler) throw(); void unexpected(); // Termination handling typedef void (*terminate_handler)(); terminate_handler set_terminate(terminate_handler) throw(); void terminate(); bool uncaught_exception() throw(); } |
标准类 exception 是所选语言构造或 C++ 标准库抛出的所有异常的基类。可以构造、复制及销毁类型为 exception 的对象,而不会生成异常。虚拟成员函数 what() 返回描述异常的字符串。
为了与 C++ 4.2 版中所用的异常兼容,还提供了头文件 <exception.h> 以用于标准模式下。该头文件允许转换到标准 C++ 代码,并包含了不是标准 C++ 部分的声明。应在开发进度计划许可的情况下,更新代码以遵循 C++ 标准(使用 <exception> 而非 <exception.h>)。
// header <exception.h>, used for transition #include <exception> #include <new> using std::exception; using std::bad_exception; using std::set_unexpected; using std::unexpected; using std::set_terminate; using std::terminate; typedef std::exception xmsg; typedef std::bad_exception xunexpected; typedef std::bad_alloc xalloc; |
在兼容模式 (—compat[=4]) 下,头文件 <exception> 不可用,头文件 <exception.h> 指随 C++ 4.2 版提供的相同头文件。此处不再重述。
可以在会出现异常的程序中使用 setjmp/longjmp 函数,只要它们不会互相影响。
使用异常和 setjmp/longjmp 的所有规则分别适用。此外,仅当在点 A 抛出与在点 B 捕获的异常具有相同的效果时,从点 A 到点 B 的 longjmp 才有效。需特别指出的是,不得使用 longjmp 进入或跳出 try 块或 catch 块(直接或间接),或使用 longjmp 跳过自动变量或临时变量的初始化或 non-trivial 销毁。
不能从信号处理程序抛出异常。
切勿将 -Bsymbolic 用于包含 C++ 代码的程序,而应使用链接程序映射文件或链接程序作用域选项(请参见4.1 链接程序作用域)。如果使用 -Bsymbolic,不同模块中的引用会绑定到应是一个全局对象内容的不同副本。
异常机制依赖对地址的比较。如果您具有某项内容的两个副本,它们的地址就不等同且异常机制可能失败,这是由于异常机制依赖对假设为唯一地址内容的比较。
本章讨论 C++ 标准中较新的强制类型转换运算符: const_cast、reinterpret_cast、static_cast 和 dynamic_cast。强制类型转换可以将对象或值从一种类型强制转换为另一种类型。
这些强制类型转换操作比以前的强制类型转换操作更好控制。dynamic_cast<> 运算符提供了一种检查指向多态类的指针的实际类型的方法。可以用文本编辑器搜索所有新式强制类型转换(搜索 _cast),而查找旧式强制类型转换需要进行语法分析。
否则,新的强制类型转换全部执行传统强制类型转换符号允许的强制类型转换子集。例如,const_cast<int*>(v) 可以写为 (int*)v。新的强制类型转换仅将各种可用的操作分类以更清楚地表示您的意图,并允许编译器提供更完善的检查。
强制类型转换运算符是始终启用的。强制类型转换符不能被禁用。
可以使用表达式 const_cast<T>(v) 更改指针或引用的 const 或 volatile 限定符。(在新式强制类型转换中,只有 const_cast<> 可以删除 const 限定符。)T 必须是指针、引用或指向成员的指针类型。
class A { public: virtual void f(); int i; }; extern const volatile int* cvip; extern int* ip; void use_of_const_cast() { const A a1; const_cast<A&>(a1).f(); // remove const ip = const_cast<int*> (cvip); // remove const and volatile } |
表达式 reinterpret_cast<T>(v) 用于更改对表达式 v 值的解释。该表达式可用于在指针和整型之间,在不相关的指针类型之间,在指向成员的指针类型之间,和在指向函数的指针类型之间转换类型。
使用 reinterpret_cast 运算符可能会得到未定义的结果或实现相关结果。以下几点描述了唯一确定的行为:
指向数据对象或函数的指针(但不是指向成员的指针)可以转换为足够包含该指针的任何整型。(long 类型总是足以包含 C++ 编译器支持的体系结构上的指针值。)转换回原始类型时,指针值将与原始指针值相比较。
指向(非成员)函数的指针可以转换为指向不同(非成员)函数类型的指针。如果转换回原始类型,指针值将与原始指针相比较。
假设新类型的对齐要求没有原始类型严格,则指向对象的指针可以转换为指向不同对象类型的指针。转换回原始类型时,指针值将与原始指针值相比较。
如果可以使用重新解释强制类型转换将“指向 T1 的指针”类型的表达式转换为“指向 T2 的指针”类型的表达式,则 T1 类型左值可以转换为“对 T2 的引用”类型。
如果 T1 和 T2 都是函数类型或都是对象类型,则“指向 T1 类型的 X 的成员的指针”类型右值可以显式转换为“指向 T2 类型的 Y 的成员的指针”类型右值。
在所有允许的情况下,空指针类型转换为不同的空指针类型后仍然是空指针。
reinterpret_cast 运算符不能用来删除 const,可使用 const_cast 来实现。
reinterpret_cast 运算符不能用来在指向同一类分层结构中不同类的指针之间进行转换,可使用静态或动态强制类型转换来实现。(reinterpret_cast 不执行所需的调整。)这一点在以下示例中描述:
class A {int a; public: A();}; class B: public A {int b, c;}; void use_of_reinterpret_cast() { A a1; long l = reinterpret_cast<long>(&a1); A* ap = reinterpret_cast<A*>(l); // safe B* bp = reinterpret_cast<B*>(&a1); // unsafe const A a2; ap = reinterpret_cast<A*>(&a2); // error, const removed } |
表达式 static_cast<T>(v) 用于将表达式 v 的值转换为 T 类型。该表达式可用于任何隐式允许的转换类型。此外,任何值的类型都可以强制转换为 void,并且,如果强制类型转换与旧式强制类型转换一样合法,则任何隐式转换都可以反向执行。
class B {...}; class C: public B {...}; enum E {first=1, second=2, third=3}; void use_of_static_cast(C* c1) { B* bp = c1; // implicit conversion C* c2 = static_cast<C*>(bp); // reverse implicit conversion int i = second; // implicit conversion E e = static_cast<E>(i); // reverse implicit conversion } |
static_cast 运算符不能用于删除 const。可以使用 static_cast 对分层结构“向下”强制类型转换(从基到派生的指针或引用),但是不会检查转换,因此结果可能无法使用。static_cast 不能用于从虚拟基类向下强制类型转换。
指向类的指针(或引用)可以实际指向(引用)从该类派生的任何类。有时希望指向完全派生类的指针,或指向完整对象的某些其他子对象。动态强制类型转换可以实现这些功能。
在兼容模式 (-compat[=4]) 下编译时,如果程序使用动态强制类型转换,则必须使用 -f eatures=rtti 进行编译。
动态强制类型转换可将指向一个类 T1 的指针(或引用)转换到指向另一个类 T2 的指针(或引用)。T1 和 T2 必须属于同一分层结构,类必须是可访问的(通过公共派生),并且转换必须明确。此外,除非转换是从派生类到其一个基类,否则,包括 T1 和 T2 的分层结构的最小部分必须是多态的(至少有一个虚函数)。
在表达式 dynamic_cast<T>(v) 中,v 是要进行强制类型转换的表达式,T 是要转换成的类型。T 必须是指向完整类的类型(其定义可见)的指针或引用,或指向 cv void 的指针,其中 cv 是空字符串、const、volatile 或 const volatile。
对分层结构进行向上强制类型转换时,如果 T 指向(或引用)v 所指向(引用)类型的基类,则该转换等效于 static_cast<T>(v)。
如果 T 是 void*,则结果是指向完整对象的指针。也就是说,v 可能指向某完整对象的其中一个基类。在这种情况下,dynamic_cast<void*>(v) 的结果如同将 v 沿分层结构向下转换为完整对象(无论什么对象)的类型,然后转换为 void*。
强制类型转换到 void* 时,分层结构必须是多态的(有虚函数)。
向下或交叉强制类型转换分层结构时,分层结构必须是多态的(具有虚函数)。结果在运行时检查。
对分层结构向下或交叉强制类型转换时,有时不能从 v 转换到 T。例如,尝试的转换可能不明确,T 可能无法访问,或 v 可能未指向(或引用)必要类型的对象。如果运行时检查失败且 T 是指针类型,则强制类型转换表达式的值是 T 类型的空指针。如果 T 是引用类型,不会返回任何值(没有 C++ 中的空引用),并且会抛出标准异常 std::bad_cast。
例如,此公共派生的示例成功:
#include <assert.h> #include <stddef.h> // for NULL class A {public: virtual void f();}; class B {public: virtual void g();}; class AB: public virtual A, public B {}; void simple_dynamic_casts() { AB ab; B* bp = &ab; // no casts needed A* ap = &ab; AB& abr = dynamic_cast<AB&>(*bp); // succeeds ap = dynamic_cast<A*>(bp); assert(ap!= NULL); bp = dynamic_cast<B*>(ap); assert(bp!= NULL); ap = dynamic_cast<A*>(&abr); assert(ap!= NULL); bp = dynamic_cast<B*>(&abr); assert(bp!= NULL); } |
但此示例失败,因为基类 B 不可访问。
#include <assert.h> #include <stddef.h> // for NULL #include <typeinfo> class A {public: virtual void f() {}}; class B {public: virtual void g() {}}; class AB: public virtual A, private B {}; void attempted_casts() { AB ab; B* bp = (B*)&ab; // C-style cast needed to break protection A* ap = dynamic_cast<A*>(bp); // fails, B is inaccessible assert(ap == NULL); try { AB& abr = dynamic_cast<AB&>(*bp); // fails, B is inaccessible } catch(const std::bad_cast&) { return; // failed reference cast caught here } assert(0); // should not get here } |
如果在一个单独的基类中存在虚拟继承和多重继承,那么实际动态强制类型转换必须能够识别出唯一的匹配。如果匹配不唯一,则强制类型转换失败。例如,假定有如下附加类定义:
class AB_B: public AB, public B {}; class AB_B__AB: public AB_B, public AB {}; |
示例:
void complex_dynamic_casts() { AB_B__AB ab_b__ab; A*ap = &ab_b__ab; // okay: finds unique A statically AB*abp = dynamic_cast<AB*>(ap); // fails: ambiguous assert(abp == NULL); // STATIC ERROR: AB_B* ab_bp = (AB_B*)ap; // not a dynamic cast AB_B*ab_bp = dynamic_cast<AB_B*>(ap); // dynamic one is okay assert(ab_bp!= NULL); } |
dynamic_cast 返回的空指针错误可用作两个代码体之间的条件,一个用于类型确定正确时处理强制类型转换,另一个用于类型确定错误时处理强制类型转换。
void using_dynamic_cast(A* ap) { if (AB *abp = dynamic_cast<AB*>(ap)) { // abp is non-null, // so ap was a pointer to an AB object // go ahead and use abp process_AB(abp);} else { // abp is null, // so ap was NOT a pointer to an AB object // do not use abp process_not_AB(ap); } } |
在兼容模式 (-compat[=4]) 下,如果尚未使用 -features=rtti 编译器选项启用运行时类型信息,则编译器将 dynamic_cast 转换到 static_cast 并发出警告。
如果禁用了异常,编译器会将 dynamic_cast<T&> 转换为 static_cast<T&> 并发出警告。(对引用类型的 dynamic_cast 要求在运行时发现转换无效的情况下抛出异常。)有关异常的信息,请参见7.5.3 诊断有问题的搜索。
动态强制类型转换需要比对应的设计模式慢,例如虚函数的转换。请参见由 Erich Gamma 编著的《Design Patterns: Elements of Reusable Object-Oriented Software》(Addison-Wesley 出版,1994)。
采用编译器易于编译优化的方式编写函数,可以改善 C++ 函数的性能。很多书中都对软件性能做了一般性介绍并具体介绍了 C++,本章不再重复这类重要信息,而只讨论那些显著影响 C++ 编译器的性能技术。
C++ 函数经常会产生必须创建并销毁的隐式临时对象。对于重要的类,临时对象的创建和销毁会占用很多处理时间和内存。C++ 编译器消除了某些临时类,但是并不能消除所有的临时类。
您编写的函数要将临时对象的数目减少到理解程序所需的最小数目。这些技术包括:使用显式变量而不使用隐式临时对象,以及使用引用变量而不使用值参数。另外一种技术是实现和使用诸如 += 这样的操作,而非实现和使用只包含 + 和 = 的操作。例如,下面的第一行引入了用于保存 a + b 结果的临时对象,而第二行则不是。
T x = a + b; T x(a); x += b; |
使用扩展内联而不使用正常调用时,对小而快速的函数的调用可以更小更快速。反过来,如果使用扩展内联而不建立分支,则对又长又慢的函数的调用会更大更慢。另外,只要函数定义更改,就必须重新编译对内联函数的所有调用。因此,使用内联函数时要格外小心。
如果预计函数定义会更改而且重新编译所有调用程序非常耗时,请不要使用内联函数。而如果扩展函数内联的代码比调用函数的代码少,或使用函数内联时应用程序执行速度显著提高,则可以使用内联函数。
编译器不能内联所有函数调用,因此要充分利用函数内联,可能需要进行一些源码更改。可使用 +w 选项了解何时不会进行函数内联。在以下情况中,编译器将不会内联函数:
函数包含了复杂控制构造,例如循环、switch 语句和 try/catch 语句。这些函数很少多次执行复杂控制构造。要内联这种函数,请将函数分割为两部分,里边的部分包含了复杂控制构造,而外边的部分决定了是否调用里边的部分。即使编译器可以内联完整函数,从函数常用部分中分隔出不常用部分的这种技术也可以改善性能。
内联函数体又大又复杂。因为对函数体内其他内联函数的调用,或因为隐式构造函数和析构函数调用(通常发生在派生类的构造函数和析构函数中),所以简单函数体可以非常复杂。对于这种函数,内联扩展很少提供显著的性能改善,所以函数一般不内联。
内联函数调用的参数既大又复杂。对于内联成员函数调用的对象是内联函数调用的自身这种情况,编译器特别敏感。要内联具有复杂参数的函数,只需将函数参数计算到局部变量并将变量传递到函数。
如果类定义不声明无参数的构造函数、复制构造函数、复制赋值运算符或析构函数,那么编译器将隐式声明它们。它们都是调用的缺省运算符。类似 C 的结构具有这些缺省运算符。编译器生成缺省运算符时,可以了解大量关于需要处理的工作和可以产生优良代码的工作。这种代码通常比用户编写的代码的执行速度快,原因是编译器可以利用汇编级功能的优点,而程序员则不能利用该功能的优点。因此缺省运算符执行所需的工作时,程序不能声明这些运算符的用户定义版本。
缺省运算符是内联函数,因此内联函数不合适时不使用缺省运算符(请参见上一节)。否则,缺省运算符是合适的:
用户编写的无参数构造函数仅为构造函数的基对象和成员变量调用无参数构造函数。有效的基元类型具有“不进行任何操作”的无参数构造函数。
用户编写的复制构造函数仅复制所有的基对象和成员变量。
用户编写的复制赋值运算符仅复制所有的基对象和成员变量。
用户编写的析构函数可以为空。
某些 C++ 编程手册建议编写类的程序员始终定义所有的运算符,以便该代码的任何读者都能了解该程序员没有忘记考虑缺省运算符的语义。显然,该建议与以上讨论的优化有冲突。这种冲突的解决方案是在代码中放置注释以表明类正使用缺省运算符。
包括结构和联合在内的 C++ 类通过值来传递和返回。对于 Plain-Old-Data (POD) 类,C++ 编译器需要像 C 编译器一样传递结构。这些类的对象直接进行传递。对于使用了用户定义复制构造函数的类的对象,编译器需要构造对象的副本,将指针传递到副本,并在返回后销毁副本。这些类的对象间接进行传递。编译器也可以选择介于这两个需求之间的类。不过,该选择影响二进制的兼容性,因此编译器对每个类的选择必须保持一致。
对于大多数编译器,直接传递对象可以加快执行速度。这种执行速度的改善对于小值类(例如复数和概率值)来说尤其明显。有时为了改善程序执行效率,您可以设计更可能直接传递而不是间接传递的类。
在兼容模式 (-compat[=4]) 下,如果类具有以下任何一项,则间接进行传递:
用户定义的构造函数
虚函数
虚拟基类
间接传递的基
间接传递的非静态数据成员
否则,类被直接传递。
在标准模式(缺省模式)中,如果类具有以下任何一条,则间接传递该类:
用户定义的复制构造函数
用户定义的析构函数
间接传递的基
间接传递的非静态数据成员
否则,类被直接传递。
尽可能直接传递类:
只要可能,就使用缺省构造函数,尤其是缺省复制构造函数。
尽可能使用缺省析构函数。缺省析构函数不是虚拟的,因此具有缺省析构函数的类通常不是基类。
避免使用虚函数和虚拟基。
C++ 编译器直接传递的类(和联合)与 C 编译器传递结构(或联合)完全相同。不过,C++ 结构和联合在不同的体系结构上进行不同的传递。
表 10–1 在不同体系结构上结构和联合的传递
体系结构 |
说明 |
---|---|
SPARC V7/V8 |
通过在调用方内分配存储并将指针传递到该存储,传递并返回结构和联合。(也就是说,所有的结构和联合都通过引用传递。) |
SPARC V9 |
不超过 16 个字节(32 个字节)的结构在寄存器中传递。通过在调用方内分配存储并将指针传递到该存储,联合和所有其他结构将被传递并返回。(也就是说,小的结构在寄存器中传递,而联合和大的结构通过引用传递。)因此,小值类与基元类具有相同的传递效率。 |
x86 平台 |
结构和联合通过在堆栈上分配空间并将参数复制到堆栈上来传递。通过在调用程序的帧中分配临时对象并将临时对象的地址作为隐式的第一个参数传递,返回结构和联合。 |
访问成员变量是 C++ 成员函数的通用操作。
编译器必须经常通过 this 指针从内存装入成员变量。因为值通过指针装入,所以编译器有时不能决定何时执行第二次装入或以前装入的值是否仍然有效。在这些情况下,编译器必须选择安全但缓慢的方法,在每次访问成员变量时重新装入成员变量。
如下所示,可以通过在局部变量中显式缓存成员变量的值来避免不必要的内存重新装入:
声明局部变量并使用成员变量的值初始化该变量。
在函数中成员变量的位置使用局部变量。
如果局部变量变化,那么将局部变量的最终值赋值到成员变量。不过,如果成员函数在该对象上调用另一个成员函数,那么该优化会产生不可预料的结果。
当值位于寄存器中时,这种优化最有效,而这种情况也与基元类型相同。基于内存的值的优化也会很有效,因为减少的别名使编译器获得了更多的机会来进行优化。
如果成员变量经常通过引用(显式或隐式)来传递,那么优化可能并没什么效果。
有时,类的目标语义需要成员变量的显式缓存,例如在当前对象和其中一个成员函数参数之间有潜在别名时。例如:
complex& operator*= (complex& left, complex& right) { left.real = left.real * right.real + left.imag * right.imag; left.imag = left.real * right.imag + left.image * right.real; } |
会产生不可预料的结果,前提是调用时使用:
x*=x; |
本章解释了如何生成多线程程序。此外,还讨论了异常的使用,解释了如何在线程之间共享 C++ 标准库对象,此外还描述了如何在多线程环境中使用传统(旧的)iostream。
关于多线程的更多信息,请参见《多线程编程指南》、《Tools.h++ 用户指南》和《标准 C++ 库用户指南》。
另请参见《OpenMP API 用户指南》,了解有关使用 OpenMP 共享内存并行化指令来创建多线程程序的信息。
C++ 编译器附带的所有库都是多线程安全的。如果需要生成多线程应用程序,或者需要将应用程序链接到多线程库,必须使用 –mt 选项编译和链接程序。此选项会将 –D_REENTRANT 传递给预处理程序,并按正确的顺序将 –lthread 传递给 ld。在兼容模式 (–compat[=4]) 下,–mt 选项可确保在 libC 之前链接 libthread。在标准模式(缺省模式)下,-mt 选项可确保 libthread 在 libCrun 之前链接。推荐使用 —mt,这是指定宏和库的替代方式,它更加简单且不易出错。
可以通过使用 ldd 命令检查应用程序是否链接到 libthread:
example% CC -mt myprog.cc example% ldd a.out libm.so.1 => /usr/lib/libm.so.1 libCrun.so.1 => /usr/lib/libCrun.so.1 libthread.so.1 => /usr/lib/libthread.so.1 libc.so.1 => /usr/lib/libc.so.1 libdl.so.1 => /usr/lib/libdl.so.1 |
C++ 支持库 libCrun、libiostream、libCstd 和 libC 是多线程安全的,但不是异步安全。这意味着,在多线程应用程序中,支持库中可用的函数不能用于信号处理程序中。这样做的话将导致死锁状态。
在多线程应用程序的信号处理程序中使用下列内容是不安全的:
iostream
new 和 delete 表达式
异常
对于多线程而言,当前的异常处理实现是安全的,一个线程中的异常与其他线程中的异常互不影响。不过,您不能使用异常来进行线程之间的通信;一个线程中抛出的异常不会被其他线程捕获到。
每个线程都可设置其自己的 terminate() 或 unexpected() 函数。在一个线程中调用 set_terminate() 或 set_unexpected() 只影响该线程中的异常。对于任何线程,terminate() 的缺省函数是 abort()(请参见8.2 指定运行时错误)。
通过调用 pthread_cancel(3T) 取消线程会导致销毁栈上的自动(局部非静态)对象,但指定了 -noex 或 -features=no%except 时例外。
pthread_cancel(3T) 使用的机制与异常相同。取消线程时,局部析构函数与用户通过 pthread_cleanup_push() 注册的清除例程交叉执行。在特定的清除例程注册之后,函数调用的本地对象在例程执行前就被销毁了。
C++ 标准库(libCstd -library=Cstd)是 MT 安全的(有些语言环境下例外),确保了在多线程环境中库内部正常工作。但是,您仍需要将各个线程之间要共享的库对象锁定起来。请参见 setlocale(3C) 和 attributes(5) 手册页。
例如,如果实例化字符串,然后创建新的线程并使用引用将字符串传递给线程。因为要在线程之间显示共享这个字符串对象,所以您必须锁定对于该字符串的写访问。(库提供的用于完成该任务的工具在下文中会有描述。)
另一方面,如果将字符串按值传递给新线程,即使两个不同的线程中的字符串应用 Rogue Wave 的“copy on write(写时复制)”技术共享表示,也不必担心锁定。库将自动处理锁定。只有当要使对象显式可用于多线程或在线程之间传递引用,以及使用全局或静态对象时,您才需要锁定。
下文描述了 C++ 标准库内部使用的锁定(同步)机制,该机制用于确保在多线程下出现正确的行为。
_RWSTDMutex 和 _RWSTDGuard 这两个同步类提供了实现多线程安全的机制。
_RWSTDMutex 类通过下列成员函数提供了与平台无关的锁定机制:
void acquire()-自己获取锁定,即在获得此类锁定之前一致处于阻塞状态。
void release()-自己释放锁定。
class _RWSTDMutex { public: _RWSTDMutex (); ~_RWSTDMutex (); void acquire (); void release (); }; |
_RWSTDGuard 类是封装有 _RWSTDMutex 类的对象的公用包装器类。_RWSTDGuard 对象尝试在其构造函数中获取封装的互斥锁(抛出从 std::exception on error 派生的 ::thread_error 类型的异常),并在析构函数中释放互斥锁(析构函数从来不会抛出异常)。
class _RWSTDGuard { public: _RWSTDGuard (_RWSTDMutex&); ~_RWSTDGuard (); }; |
另外,可以使用宏 _RWSTD_MT_GUARD(mutex)(以前的 _STDGUARD)有条件地在多线程生成中创建 _RWSTDGuard 的对象。该对象保护代码块的其余部分,并在该代码块中定义为可同时被多个线程执行。在单线程生成中,宏扩展到空表达式中。
以下示例说明了这些机制的使用。
#include <rw/stdmutex.h> // // An integer shared among multiple threads. // int I; // // A mutex used to synchronize updates to I. // _RWSTDMutex I_mutex; // // Increment I by one. Uses an _RWSTDMutex directly. // void increment_I () { I_mutex.acquire(); // Lock the mutex. I++; I_mutex.release(); // Unlock the mutex. } // // Decrement I by one. Uses an _RWSTDGuard. // void decrement_I () { _RWSTDGuard guard(I_mutex); // Acquire the lock on I_mutex. --I; // // The lock on I is released when destructor is called on guard. // } |
本节介绍如何将 libC 和 libiostream 库的 iostream 类用于多线程环境的输入输出 (input-output, I/O)。另外,还提供了如何通过从 iostream 类派生来扩展库的功能的示例。但本节并不是指导如何采用 C++ 编写多线程代码。
此处的讨论只适用于原来的 iostream(libC 和 libiostream),而不适用于 libCstd(即新的 iostream,它是 C++ 标准库的一部分)。
iostream 库允许多线程环境中的应用程序使用其接口,以及运行支持的 Solaris 操作系统版本时使用多线程功能的程序使用其接口。如果应用程序使用以前版本库的单线程功能,那么该应用程序不会受到影响。
如果库能在有多个线程的环境中正常运行,则该库定义为 MT 安全的。通常,此处的“正常”意味着其所有公用函数都是可重入的。iostream 库提供了保护,防止多个线程尝试修改由多个线程共享的对象(即 C++ 类的实例)的状态。但 iostream 对象的 MT 安全作用域仅限于该对象的公共成员函数正在执行的那一段时间。
应用程序并不能因为使用 libC 库中的 MT 安全对象,而被自动保证为是 MT 安全的。应用程序只有按预期那样能够在多线程环境中执行时,才被定义为 MT 安全的。
MT 安全的 iostream 库的组织与其他版本的 iostream 库稍有不同。库的导出接口指的是 iostream 类的受保护的公共成员函数以及可用基类集合,这一点与其他版本的库相同;但类的分层结构是不同的。有关详细信息,请参见11.4.2 iostream 库接口更改。
原来的核心类已重命名,即添加了前缀 unsafe_。表 11–1 列出了属于 iostream 软件包核心的类。
表 11–1 原来的 iostream 核心类
类 |
说明 |
---|---|
stream_MT |
多线程安全类的基类。 |
streambuf |
缓冲区的基类。 |
unsafe_ios |
该类包含各种流类通用的状态变量;例如,错误和格式化状态。 |
unsafe_istream |
该类支持从 streambuf 检索的字符序列的有格式和无格式转换。 |
unsafe_ostream |
该类支持存储到 streambuf 中的字符序列的有格式和无格式转换。 |
unsafe_iostream |
该类合并 unsafe_istream 类和 unsafe_ostream 类,以便进行双向操作。 |
每个 MT 安全的类都是从基类 stream_MT 派生而来。每个 MT 安全的类(除了 streambuf 外)也都是从现有的 unsafe_ 基类派生而来。示例如下:
class streambuf: public stream_MT {...}; class ios: virtual public unsafe_ios, public stream_MT {...}; class istream: virtual public ios, public unsafe_istream {...}; |
类 stream_MT 提供了使每个 iostream 类成为 MT 安全的类所需的互斥锁,另外,还提供了动态启用和禁用这些锁的功能,以便可以动态更改 MT 安全属性。用于 I/O 转换和缓冲区管理的基本功能划入 unsafe_ 类,为库增加 MT 安全的功能则划入派生类。每个类的 MT 安全版本都包含与 unsafe_ base 类相同的受保护的公共成员函数。MT 安全版本类中的每个成员函数都可用作包装器,它可以锁定对象、调用 unsafe_ base 中的相同函数以及解锁对象。
streambuf 类不是从非安全类派生而来的。streambuf 类的受保护的公共成员函数可以通过锁定来重入。此外还提供了带 _unlocked 后缀的已解锁版本。
已向 iostream 接口添加了一组 MT 安全的重入公共函数。 用户指定的缓冲区被作为每个函数的附加参数。这些函数如下所述:
表 11–2 多线程安全的可重入公共函数
功能 |
说明 |
---|---|
char *oct_r (char *buf, int buflen, long num, int width) |
将指针返回到用八进制表示数字的 ASCII 字符串。非零宽度假定为格式化的字段宽度。返回值不保证指向用户提供缓冲区的开始部分。 |
char *hex_r (char *buf, int buflen, long num, int width) |
将指针返回到用十六进制表示数字的 ASCII 字符串。非零宽度假定为格式化的字段宽度。返回值不保证指向用户提供缓冲区的开始部分。 |
char *dec_r (char *buf, int buflen, long num, int width) |
将指针返回到用十进制表示数字的 ASCII 字符串。非零宽度假定为格式化的字段宽度。返回值不保证指向用户提供缓冲区的开始部分。 |
char *chr_r (char *buf, int buflen, long num, int width) |
返回指向包含字符 chr 的 ASCII 字符串的指针。如果宽度非零,则字符串包含后跟 chr 的 width 个空格。返回值不保证指向用户提供缓冲区的开始部分。 |
char *form_r (char *buf, int buflen, long num, int width) |
返回由 sprintf 格式化字符串的指针,其中使用了格式字符串 format 和其余参数。缓冲区必须具有足够的空间以包含格式化的字符串。 |
用来确保与早期版本的 libC 兼容的 iostream 库的公共转换例程(oct、hex、dec、chr 和 form)不是 MT 安全的。
生成使用 libC 库的 iostream 类以在多线程环境中运行的应用程序时,应使用 -mt 选项编译和链接该应用程序的源代码。此选项可将 -D_REENTRANT 传递给预处理程序,并将 -lthread 传递给链接程序。
请使用 -mt(而不是 -lthread)与 libC 和 libthread 链接。该选项确保了库的正确链接顺序。错误使用 -lthread 可能会导致应用程序无法正常运行。
对于使用 iostream 类 的单线程应用程序,不需要使用特殊的编译器和链接程序选项。缺省情况下,编译器会与 libC 库链接。
有关 iostream 库的 MT 安全性的限制定义意味着,用于 iostream 的许多编程常用方式在使用共享 iostream 对象的多线程环境中是不安全的。
要实现 MT 安全,必须在具有可能导致出现错误的 I/O 操作的关键区中进行错误检查。以下示例说明了如何检查错误:
#include <iostream.h> enum iostate {IOok, IOeof, IOfail}; iostate read_number(istream& istr, int& num) { stream_locker sl(istr, stream_locker::lock_now); istr >> num; if (istr.eof()) return IOeof; if (istr.fail()) return IOfail; return IOok; } |
在此示例中,stream_locker 对象 sl 的构造函数锁定 istream 对象 istr。在 read_number 终止时调用的析构函数 sl 解锁 istr。
要实现 MT 安全,必须在执行上次输入操作和 gcount 调用这一期间独占使用 istream 对象的线程内调用 gcount 函数。以下示例说明了对 gcount 的调用:
#include <iostream.h> #include <rlocks.h> void fetch_line(istream& istr, char* line, int& linecount) { stream_locker sl(istr, stream_locker::lock_defer); sl.lock(); // lock the stream istr istr >> line; linecount = istr.gcount(); sl.unlock(); // unlock istr ... } |
在此示例中,stream_locker 类的成员函数 lock 和 unlock 定义了程序中的互斥区域。
要实现 MT 安全,必须锁定为用户定义类型定义且涉及对各个操作进行特定排序的 I/O 操作,才能定义关键区。以下示例说明了用户定义的 I/O 操作:
#include <rlocks.h> #include <iostream.h> class mystream: public istream { // other definitions... int getRecord(char* name, int& id, float& gpa); }; int mystream::getRecord(char* name, int& id, float& gpa) { stream_locker sl(this, stream_locker::lock_now); *this >> name; *this >> id; *this >> gpa; return this->fail() == 0; } |
使用此版本的 libC 库中的 MT 安全类会导致一些性能开销,即使是单线程应用程序中也是如此,但如果使用 libC 的 unsafe_ 类,则可避免此开销。
可以使用作用域解析运算符执行基类 unsafe_ 的成员函数,例如:
cout.unsafe_ostream::put(’4’); |
cin.unsafe_istream::read(buf, len); |
unsafe_ 类不能在多线程应用程序中安全地使用。
可以使 cout 和 cin 对象成为不安全对象,然后执行正常操作,而不是使用 unsafe_ 类。这会稍微降低性能。以下示例说明了如何使用 unsafe cout 和 cin:
#include <iostream.h> //disable mt-safety cout.set_safe_flag(stream_MT::unsafe_object); //disable mt-safety cin.set_safe_flag(stream_MT::unsafe_object); cout.put(”4’); cin.read(buf, len); |
iostream 对象是 MT 安全对象时,有互斥锁定保护对象的成员变量。该锁定给仅在单线程环境中执行的应用程序增加了不必要的开销。为了提高性能,可以动态地启用或禁用 iostream 对象的 MT 安全性。以下示例使 iostream 对象成为 MT 不安全的对象:
fs.set_safe_flag(stream_MT::unsafe_object);// disable MT-safety .... do various i/o operations |
可以在多个线程未共享 iostream 的情况下(例如,在只有一个线程的程序中,或在每个 iostream 都是线程专用的程序中),在代码中安全地使用 MT 不安全的流。
如果显式在程序中插入同步,还可以在多个线程共享 iostream 的环境中安全地使用 MT 不安全的 iostream。以下示例说明了该技术:
generic_lock(); fs.set_safe_flag(stream_MT::unsafe_object); ... do various i/o operations generic_unlock(); |
其中,函数 generic_lock 和 generic_unlock 可以是使用诸如互斥锁、信号或读取器/写入器锁定等基元的任何同步机制。
libC 库提供的 stream_locker 类是实现这一目的的首选机制。
有关更多信息,请参见11.4.5 对象锁定。
本节介绍为使 iostream 库成为 MT 安全库而对其所做的接口更改。
stream_MT stream_locker unsafe_ios unsafe_istream unsafe_ostream unsafe_iostream unsafe_fstreambase unsafe_strstreambase |
下表列出了已增加到 iostream 接口的新类的分层结构。
class streambuf: public stream_MT {...}; class unsafe_ios {...}; class ios: virtual public unsafe_ios, public stream_MT {...}; class unsafe_fstreambase: virtual public unsafe_ios {...}; class fstreambase: virtual public ios, public unsafe_fstreambase {...}; class unsafe_strstreambase: virtual public unsafe_ios {...}; class strstreambase: virtual public ios, public unsafe_strstreambase {...}; class unsafe_istream: virtual public unsafe_ios {...}; class unsafe_ostream: virtual public unsafe_ios {...}; class istream: virtual public ios, public unsafe_istream {...}; class ostream: virtual public ios, public unsafe_ostream {...}; class unsafe_iostream: public unsafe_istream, public unsafe_ostream {...}; |
class streambuf { public: int sgetc_unlocked(); void sgetn_unlocked(char *, int); int snextc_unlocked(); int sbumpc_unlocked(); void stossc_unlocked(); int in_avail_unlocked(); int sputbackc_unlocked(char); int sputc_unlocked(int); int sputn_unlocked(const char *, int); int out_waiting_unlocked(); protected: char* base_unlocked(); char* ebuf_unlocked(); int blen_unlocked(); char* pbase_unlocked(); char* eback_unlocked(); char* gptr_unlocked(); char* egptr_unlocked(); char* pptr_unlocked(); void setp_unlocked(char*, char*); void setg_unlocked(char*, char*, char*); void pbump_unlocked(int); void gbump_unlocked(int); void setb_unlocked(char*, char*, int); int unbuffered_unlocked(); char *epptr_unlocked(); void unbuffered_unlocked(int); int allocate_unlocked(int); }; class filebuf: public streambuf { public: int is_open_unlocked(); filebuf* close_unlocked(); filebuf* open_unlocked(const char*, int, int = filebuf::openprot); filebuf* attach_unlocked(int); }; class strstreambuf: public streambuf { public: int freeze_unlocked(); char* str_unlocked(); }; unsafe_ostream& endl(unsafe_ostream&); unsafe_ostream& ends(unsafe_ostream&); unsafe_ostream& flush(unsafe_ostream&); unsafe_istream& ws(unsafe_istream&); unsafe_ios& dec(unsafe_ios&); unsafe_ios& hex(unsafe_ios&); unsafe_ios& oct(unsafe_ios&); char* dec_r (char* buf, int buflen, long num, int width) char* hex_r (char* buf, int buflen, long num, int width) char* oct_r (char* buf, int buflen, long num, int width) char* chr_r (char* buf, int buflen, long chr, int width) char* str_r (char* buf, int buflen, const char* format, int width = 0); char* form_r (char* buf, int buflen, const char* format,...) |
全局和多线程应用程序中的静态数据不能在线程间安全共享。尽管线程独立执行,但它们在进程中共享对全局和静态对象的访问。如果一个线程修改了这种共享对象,那么进程中的其他线程将观察该更改,使得状态难以维持。在 C++ 中,类对象(类的实例)靠其成员变量的值来维持状态。如果类对象被共享,那么该类对象将易于被其他线程更改。
多线程应用程序使用 iostream 库并包含 iostream.h 时,在缺省情况下,标准流(cout、cin、cerr 和 clog)定义为全局共享对象。由于 iostream 库是 MT 安全库,它可在执行 iostream 对象的成员函数时保护其共享对象的状态不会被其他线程访问或更改。但对象的 MT 安全性作用域限于执行该对象的公共成员函数期间。例如,
int c; cin.get(c); |
获得 get 缓冲区中下一个字符,并更新 ThreadA 中的缓冲区指针。但如果 ThreadA 中的下一个指令是另一个 get 调用,则 libC 库不能保证返回序列中的下一个字符。这是因为存在一些问题,例如,ThreadB 可能也在 ThreadA 中所做的两次 get 调用之间执行了 get 调用。
更多关于共享对象和多线程问题的处理策略,请参见11.4.5 对象锁定。
通常,使用 iostream 对象时,一序列 I/O 操作必须是 MT 安全的。例如,如下所示代码:
cout << " Error message:" << errstring[err_number] << "\n"; |
涉及执行 cout 流对象的三个成员函数。由于 cout 是共享对象,因此必须独立执行序列,才能在多线程环境中正常使用关键区。要独立对 iostream 类对象执行一序列操作,必须使用某种形式的锁定。
libC 库提供了 stream_locker 类用于锁定针对 iostream 对象的操作。有关 stream_locker 类的信息,请参见11.4.5 对象锁定。
最简单的共享对象和多线程问题处理策略是通过确保 iostream 对象是线程局部对象来避免问题。例如,
不过在许多情况下(例如缺省共享标准流对象),使对象专用于某线程是不可能的,这就需要其他的策略了。
要独立对 iostream 类对象执行一序列操作,必须使用某种形式的锁定。锁定会增加一些开销,即使是在单线程应用程序中也是如此。是增加锁定还是使 iostream 对象成为线程的专用对象取决于为应用程序选择的线程模型: 线程是独立的还是协同操作的?
如果每个独立的线程都使用其自己的 iostream 对象来生成或使用数据,则这些 iostream 对象专用于各自的线程,因此不需要锁定。
如果多个线程协同操作(即,共享同一个 iostream 对象),则必须同步对共享对象的访问,而且必须使用某种形式的锁定使序列化操作独立化。
iostream 库提供了 stream_locker 类,用于锁定针对 iostream 对象的一系列操作。因此可以将动态启用或禁用 iostream 对象的锁定所造成的性能开销降到最低。
可以使用 stream_locker 类的对象使针对流对象的一序列操作独立化。例如,下例中所示代码尝试查找文件中的某一位置,并读取下一个数据块。
#include <fstream.h> #include <rlocks.h> void lock_example (fstream& fs) { const int len = 128; char buf[len]; int offset = 48; stream_locker s_lock(fs, stream_locker::lock_now); .....// open file fs.seekg(offset, ios::beg); fs.read(buf, len); } |
在此示例中,stream_locker 对象的构造函数定义了每次只能执行一个线程的互斥区域的开始位置。从函数返回后调用的析构函数定义了互斥区域的结束位置。stream_locker 对象确保了在文件中查找特定偏移和从文件中读取能够同时独立执行,并且在原来的 ThreadA 读取文件之前,ThreadB 不能更改文件偏移。
另一种使用 stream_locker 对象的方法是显式定义互斥区域。在以下示例中,为了使 I/O 操作和后续错误检查独立化,使用了 vbstream_locker 对象的成员函数 lock 和 unlock 调用。
{ ... stream_locker file_lck(openfile_stream, stream_locker::lock_defer); .... file_lck.lock(); // lock openfile_stream openfile_stream << "Value: " << int_value << "\n"; if(!openfile_stream) { file_error("Output of value failed\n"); return; } file_lck.unlock(); // unlock openfile_stream } |
有关更多信息,请参见 stream_locker(3CC4) 手册页。
可以通过派生新类来扩展 或专用化 iostream 类的功能。如果将在多线程环境中使用从派生类实例化的对象,则这些类必须是 MT 安全类。
通过保护对象的内部状态不会被多线程修改来使类对象成为 MT 安全对象。为此,应使用互斥锁序列化对受保护的公共成员函数中成员变量的访问。
通过在 stream_locker 对象定义的关键区中使用 streambuf 的成员函数 _unlocked 来避免锁定开销。
在应用程序直接调用函数情况下,锁定 streambuf 类的公共虚拟函数。这些函数包括: xsgetn、underflow、pbackfail、xsputn、overflow、seekoff 和 seekpos。
使用 ios 类中的成员函数 iword 和 pword 来扩展 ios 对象的格式化状态。但如果多个线程共享 iword 或 pword 函数的相同索引,将会出现问题。要使线程成为 MT 安全线程,请使用适当的锁定方案。
锁定返回的成员变量的值大于 char 的成员函数。
在删除多个线程共享的 iostream 对象之前,主线程必须核实子线程已完成了对共享对象的使用。下例说明了如何安全销毁共享对象。
#include <fstream.h> #include <thread.h> fstream* fp; void *process_rtn(void*) { // body of sub-threads which uses fp... } void multi_process(const char* filename, int numthreads) { fp = new fstream(filename, ios::in); // create fstream object // before creating threads. // create threads for (int i=0; i<numthreads; i++) thr_create(0, STACKSIZE, process_rtn, 0, 0, 0); ... // wait for threads to finish for (int i=0; i<numthreads; i++) thr_join(0, 0, 0); delete fp; // delete fstream object after fp = NULL; // all threads have completed. } |
以下代码是以 MT 安全方式使用来自 libC 的 iostream 对象的多线程应用程序示例。
该示例应用程序创建了多达 255 个线程。每个线程读取不同的输入文件,每次读取一行,并且使用标准输出流 cout 将行输出到输出文件。所有线程共享的输出文件用值来标记,该值表明了哪个线程执行输出操作。
// create tagged thread data // the output file is of the form: // <tag><string of data>\n // where tag is an integer value in a unsigned char. // Allows up to 255 threads to be run in this application // <string of data> is any printable characters // Because tag is an integer value written as char, // you need to use od to look at the output file, suggest: // od -c out.file |more #include <stdlib.h> #include <stdio.h> #include <iostream.h> #include <fstream.h> #include <thread.h> struct thread_args { char* filename; int thread_tag; }; const int thread_bufsize = 256; // entry routine for each thread void* ThreadDuties(void* v) { // obtain arguments for this thread thread_args* tt = (thread_args*)v; char ibuf[thread_bufsize]; // open thread input file ifstream instr(tt->filename); stream_locker lockout(cout, stream_locker::lock_defer); while(1) { // read a line at a time instr.getline(ibuf, thread_bufsize - 1, ’\n’); if(instr.eof()) break; // lock cout stream so the i/o operation is atomic lockout.lock(); // tag line and send to cout cout << (unsigned char)tt->thread_tag << ibuf << "\n"; lockout.unlock(); } return 0; } int main(int argc, char** argv) { // argv: 1+ list of filenames per thread if(argc < 2) { cout << “usage: " << argv[0] << " <files..>\n"; exit(1); } int num_threads = argc - 1; int total_tags = 0; // array of thread_ids thread_t created_threads[thread_bufsize]; // array of arguments to thread entry routine thread_args thr_args[thread_bufsize]; int i; for(i = 0; i < num_threads; i++) { thr_args[i].filename = argv[1 + i]; // assign a tag to a thread - a value less than 256 thr_args[i].thread_tag = total_tags++; // create threads thr_create(0, 0, ThreadDuties, &thr_args[i], THR_SUSPENDED, &created_threads[i]); } for(i = 0; i < num_threads; i++) { thr_continue(created_threads[i]); } for(i = 0; i < num_threads; i++) { thr_join(created_threads[i], 0, 0); } return 0; } |