Pure C++:泛型编程:模板特殊化
2008-03-08 12:24:48 来源:WEB开发网 【

核心提示: 在上一期专栏中我已经谈到过,执行的操作不仅包括简单存储和检索操作的参数化类型仅限于可安全绑定到它的可接受类型 [请参阅 Pure C++: CLR Generics Versus C++ Templates(英文)],Pure C++:泛型编程:模板特殊化,使用泛型,可以通过 where 子句显式加上这些限制,他在
在上一期专栏中我已经谈到过,执行的操作不仅包括简单存储和检索操作的参数化类型仅限于可安全绑定到它的可接受类型 [请参阅 Pure C++: CLR Generics Versus C++ Templates(英文)]。使用泛型,可以通过 where 子句显式加上这些限制。
局部模板非凡化
假如类模板有多个模板参数,您可以针对一个或一组特定的参数化值或类型来非凡化类模板。也就是说,您可能希望提供一个模板,使其除了某些模板参数已被实际类型或实际值替换以外,其他均与常规模板匹配。使用局部模板非凡化就可以实现此目的。例如,假设存在下面的 Buffer 类模板: template <class elemType, int size> ref class Buffer { ... }; 下面说明如何对 Buffer 使用局部非凡化,使其能够很好地处理大小为 1KB 的缓冲区: // 类模板 Buffer 的局部非凡化 template <class elemType> ref class Buffer<elemType,1024> { // 对 1KB 大小使用非凡算法... }; Buffer 的局部非凡化只有一个类型参数 elemType,因为大小的值固定为 1024。局部模板非凡化的参数列表只列出了模板参数仍然未知的参数。但是,当您定义该模板的实例时,必须同时指定这两个参数(这与对一个参数使用默认值的情形不同)。在下面的示例中,局部类模板非凡化是用 elemType 为 String 的类型参数实例化的: Buffer<String^,1024> mumble; 但是,假如您改为下面的代码行,则编译器会生成错误,并将声明标记为缺少第二个参数: Buffer<String^> mumble; // 错误 为什么会这样呢?假如开发人员以后引入一组非凡化的 Buffer(如下所示),会出现什么情况? template <class elemType> ref class Buffer<elemType,4096> {}; template <class elemType> ref class Buffer<elemType,512> {}; 假如前面示例的声明中不要求使用第二个参数,编译器就无法区分这几种非凡化! 局部非凡化与其对应的完整常规模板同名,在本例中为 Buffer。这就带来了一个有趣的问题。请注重,Buffer<String^,1024> 的实例化既可以通过类模板定义进行,也可以通过局部非凡化进行。那么,为什么会选择局部非凡化来实例化该模板呢?一般的规则是:假如声明了局部类模板非凡化,编译器就会选择最非凡化的模板定义进行实例化。只有在无法使用局部非凡化时,才会使用常规模板定义。 例如,当必须实例化 Buffer<String^,2048> 时,由于此实例化与任何一个局部模板非凡化都不匹配,因此会选择常规模板定义。 局部非凡化的定义完全不同于常规模板的定义。局部非凡化可以拥有一组与常规类模板完全不同的成员。局部类模板非凡化的成员函数、静态数据成员和嵌套类型必须有自己的定义,这与类模板非凡化相同。类模板成员的常规定义决不能用于实例化局部类模板非凡化的成员。 类模板的局部模板非凡化构成了现代 C++ 用法中一些非常复杂的设计惯用语的基础。假如您对此感爱好,可以阅读 Andrei Alexandrescu 撰写的《Modern C++ Design: Generic PRogramming and Design Patterns Applied》(Addison-Wesley,2001 年版),了解此用法的具体信息。
进入讨论组讨论。 函数模板非凡化 非成员函数模板也可以进行非凡化。在有些情况下,您可以充分利用有关类型的一些专门知识,来编写比从模板实例化的函数更高效的函数。在其他一些情况下,常规模板的定义对某种类型而言根本就是错误的。
假如两个参数的类型均为 String,常规函数模板不会扩展。这正是我们所需要的行为。 只要提供了显式函数模板非凡化,就必须始终指定 template<> 和函数参数列表。例如,max 的下面两个声明不合法,并且在编译时会被标记为: // 错误:无效的非凡化声明 // 缺少 template<> String^ max<String^>( String^, String^ ); // 缺少函数参数列表 template<> String^ max<String^>; 有一种情况,省略函数模板非凡化的 template<> 部分不是错误。即,在您声明的普通函数带有与模板实例化相匹配的返回类型和参数列表的情况下: // 常规模板定义 template <class T> T max( T t1, T t2 ) { /* ... */ } // 没问题:普通函数声明! String^ max( String^, String^ ); 毫无疑问,您经常会感到很无奈,并认为 C++ 真是太难理解了。您可能想知道,究竟为什么所有人都希望声明与模板实例化相匹配的普通函数,而不希望声明显式非凡化。那么,请看下面的示例,事情并不是完全按照您喜欢的方式进行的: void foo( String^ s1, String^ s2 ) { // 能否解析非凡化的实例? String^ maxString = max( "muffy", s2 ); // ... } 在 C++/CLI 下,对于重载解决方案,字符串文字的类型既是 const char[n] [其中 n 是文字的长度加一(用于终止空字符)],又是 System::String。这意味着,给定一组函数 void f( System::String^ ); // (1) void f( const char* ); // (2) void f( std::string ); // (3) 如下所示的调用 // 在 C++/CLI 下解析为 (1) f( "bud, not buddy" ); 与 (1) 完全匹配,而在 ISO-C++ 下,解析结果会是 (2)。因此,问题就是,对于函数模板的类型推断而言,字符串文字是否还是被当作 System::String 进行处理?简言之,答案是“不”。(具体的答案将是我下一期专栏的主题,该专栏将具体介绍函数模板。)因此,不选择 max 的非凡化 String 实例,下面对 max 的调用 String^ maxString = max( "muffy", s2 ); // 错误 在编译时会失败,因为 max 的定义要求两个参数的类型均为 T: template <class T> T max( T t1, T t2 ); 那您能做些什么呢?像在下面的重新声明中一样,将模板改为带有两个参数的实例 template <class T1,class T2> ??? max( T1 t1, T2 t2 ); 使我们能够编译带有 muffy 和 s2 的 max 的调用,但会因大于 (>) 运算符而断开;并且指定要返回的参数类型。 我想做的就是始终将字符串文字强制转换为 String 类型,这也是拯救普通函数的方法。 假如在推断模板参数时使用了某个参数,那么只有一组有限的类型转换可用于将函数模板实例化的参数转换为相应的函数参数类型。还有一种情况是显式非凡化函数模板。正如您所看到的,从字符串文字到 System::String 的转换不属于上述情况。 在存在有害字符串文字的情况下,显式非凡化无助于避免对类型转换的限制。假如您希望不仅答应使用一组有限的类型转换,则必须定义普通函数而不是函数模板非凡化。这就是 C++ 答应重载非模板函数和模板函数的原因。进入讨论组讨论。 我基本上已经讲完了,不过还有最后一点需要说明。创建一组您在图 5 中看到的 max 函数意味着什么?您知道调用 max( 10, 20 ); 始终会解析为常规模板定义,并将 T 推断为 int。同样,您现在还知道调用
重载函数的解析过程
解析重载函数的第一步是建立候选函数集。候选函数集包含与被调用的函数同名并且在调用时能够看到其声明的函数。 第一个可见函数是非模板实例。我将该函数添加到候选列表中。那么函数模板呢?在能够看到函数模板时,假如使用函数调用参数可以实例化函数,则该模板的实例化被视为候选函数。在我的示例中,函数参数为 s2,其类型为 String。模板参数推断将 String 绑定到 T,因此模板实例化 max(String^,String^) 将添加到候选函数集中。 只有在模板参数推断成功时,函数模板实例化才会进入候选函数集。但是,假如模板参数推断失败,不会出现错误;即,函数实例化没有添加到候选函数集中。 假如模板参数推断成功,但是模板是为推断出的模板参数显式非凡化的(正如我的示例一样),会怎么样呢?结果是,显式模板非凡化(而不是通过常规模板定义实例化的函数)将进入候选函数。 因此,此调用有两个候选函数:非凡化的模板实例化和非模板实例。 // 候选函数 // 非凡化的模板... template<> String^ max<String^>( String^ s1, String^ s2 ); // 非模板实例 String^ max( String^, String^ ); 解析重载函数的下一步是从候选函数集中选择可行函数集。对于要限定为可行函数的候选函数,必须存在类型转换,将每个实际参数类型转换为相应的形式参数类型。在该示例中,两个候选函数都是可行的。 解析重载函数的最后一步是,对参数所应用的类型转换进行分级,以选择最好的可行函数。例如,两个函数看起来都很好。既然两个函数都可行,那么这是否应该被视为不明确的调用? 实际上,调用是明确的:将调用非模板 max,因为它优先于模板实例化。原因是,在某种程度上,显式实现的函数比通过常规模板创建的实例更为实用。 令人吃惊的是,在解决有害字符串文字的情况中,我已经彻底消除了调用以前的 String 非凡化的可能性,因此我可以消除这个问题。我只需要常规模板声明以及重载的非模板实例: // 支持 String 的最终重载集 template <class T> T max( T t1, T t2 ) { /* ... */ } String^ max( String^, String^ ); 这不一定会很复杂,但有一点是肯定的 - 在语言集成和灵活性方面,它远远地超过了公共语言运行时 (CLR) 泛型功能可以支持的范围。 模板非凡化是 C++ 模板设计的基础。它提供了最好的性能,克服了对单个或系列类类型的限制,具有灵活的设计模式,并且在实际代码中已证实其巨大价值。在下一期专栏中,我将深入分析 C++/CLI 对模板函数和常规函数的支持。
请将您的疑问和意见通过 purecpp@microsoft.com 发送给 Stanley。 Stanley B. Lippman 是 Microsoft 公司 Visual C++ 团队的体系结构设计师。他从 1984 年开始在 Bell 实验室与 C++ 的设计者 Bjarne Stroustrup 一起研究 C++。此后,他在 Disney 和 DreamWorks 制作过动画,还担任过 JPL 的高级顾问和 Fantasia 2000 的软件技术主管。 转到原英文页面进入讨论组讨论。
更多精彩
赞助商链接