其他语言

位置:IT落伍者 >> 其他语言 >> 浏览文章

Pure C++:泛型编程:模板特殊化


发布日期:2019年05月27日
 
Pure C++:泛型编程:模板特殊化

在上一期专栏中我已经谈到过执行的操作不仅包括简单存储和检索操作的参数化类型仅限于可安全绑定到它的可接受类型 [请参阅 Pure C++: CLR Generics Versus C++ Templates(英文)]使用泛型可以通过 where 子句显式加上这些限制在 C++/CLI 模板工具中通过将函数模板或类模板(单个成员函数或整个类)特殊化通常可以避免这些限制例如将 min 函数添加到上一期专栏的 tStack 类中通常我会使用常规 min 算法但那种算法仅在我作为程序员的时候有用而对我撰写有关模板特殊化的文章没有帮助为了方便起见 中重现了 tStack 类的模板定义

显示了 min 的一种可能的实现方法我将定义一个局部变量 min_val 来存放最小元素并将它初始化为容器的第一个元素然后定义两个迭代程序将每个元素与 min_val 进行比较如果其值比 min_val 小则为 min_val 重新赋值现在您能看出隐含的限制吗?如果能则您会得到

if ( *it < min_val )

通常对于 min 函数只有能够使用内置小于 (<) 运算符的类型或本身具有 operator<() 实例的类型才能绑定到 elemType 的类型如果某个类型没有定义 operator<()并尝试对此类型的项的 tStack 调用 min则在 min 中使用无效的比较运算符时将出现编译时错误例如System::String 类没有小于 (<) 运算符(它使用 IComparable 的 CompareTo 方法)因此如果我尝试对使用 String 实例化的 tStack 调用 min则它在编译时就会出错因为该比较操作失败了

有一种解决方案我不会使用定义全局运算符 operator<()该运算符使用 CompareTo 来比较两个 String 类型的值然后tStack<String^>::min() 会自动调用这些全局运算符

bool operator<( String^ s String^ s ) {return ( s>CompareTo( s ) < ) ? true :false;}

请记住目标是防止当用户指定的类型参数为 String 时实例化 tStack::min 成员函数定义而希望使用 CompareTo 方法来定义自己的 tStack<String^>::min 实例您可以使用显式模板特殊化定义为类模板实例化的成员提供特殊化的定义来实现此目的此定义指明了模板名称指定模板的参数函数参数列表和函数主体关键字模板的后面是小于 (<) 和大于 (>) 标记然后是类成员特殊化的定义(请参阅图

即使类的类型 tStack<String^> 是从常规类模板定义(即由编译器内部生成的专用于 String 的实例其中每个 elemType 占位符都被替换为 String 类型)实例化的类型 tStack<String^> 的每个对象都会调用特殊化的成员函数 mintStack::min 成员函数定义既不会被扩展也不会在 tStack<String^> 中使用

在有些情况下可能整个类模板定义都不适合某种类型在这种情况下程序员可以提供一种定义来特殊化整个类模板程序员可以提供 tStack<String^> 的定义

template <class elemType>ref class tStack;// 类模板特殊化template<> ref class tStack<String^> {public:tStack();String^ pop();void push( Stack^ et );    // };

只有在声明了常规类模板后才能定义显式类模板特殊化如果您提供完整的类模板特殊化则必须定义与此特殊化关联的每个成员函数或静态数据成员类模板的常规成员定义决不能用于创建显式特殊化的成员定义也不会被交叉检查这是因为类模板特殊化的类成员集可能与常规模板的类成员集完全不同

定义完全特殊化的类模板(如 tStack<String^>)的成员时请勿在其定义前添加特殊的 template<> 标记而应该通过显式列出实际的类型来指明特殊化定义如下所示

// 定义类模板特殊化的// 成员函数 min()String^ tStack<String^>::min() { }

局部模板特殊化

如果类模板有多个模板参数您可以针对一个或一组特定的参数化值或类型来特殊化类模板也就是说您可能希望提供一个模板使其除了某些模板参数已被实际类型或实际值替换以外其他均与常规模板匹配使用局部模板特殊化就可以实现此目的例如假设存在下面的 Buffer 类模板

template <class elemType int size>ref class Buffer { };

下面说明如何对 Buffer 使用局部特殊化使其能够很好地处理大小为 KB 的缓沖区

// 类模板 Buffer 的局部特殊化template <class elemType>ref class Buffer<elemType> {// 对 KB 大小使用特殊算法};

Buffer 的局部特殊化只有一个类型参数 elemType因为大小的值固定为 局部模板特殊化的参数列表只列出了模板参数仍然未知的参数但是当您定义该模板的实例时必须同时指定这两个参数(这与对一个参数使用默认值的情形不同)在下面的示例中局部类模板特殊化是用 elemType 为 String 的类型参数实例化的

Buffer<String^> mumble;

但是如果您改为下面的代码行则编译器会生成错误并将声明标记为缺少第二个参数

Buffer<String^> mumble;  // 错误

为什么会这样呢?如果开发人员以后引入一组特殊化的 Buffer(如下所示)会出现什么情况?

template <class elemType>ref class Buffer<elemType> {};template <class elemType> ref class Buffer<elemType> {};

如果前面示例的声明中不要求使用第二个参数编译器就无法区分这几种特殊化!

局部特殊化与其对应的完整常规模板同名在本例中为 Buffer这就带来了一个有趣的问题请注意Buffer<String^> 的实例化既可以通过类模板定义进行也可以通过局部特殊化进行那么为什么会选择局部特殊化来实例化该模板呢?一般的规则是如果声明了局部类模板特殊化编译器就会选择最特殊化的模板定义进行实例化只有在无法使用局部特殊化时才会使用常规模板定义

例如当必须实例化 Buffer<String^> 时由于此实例化与任何一个局部模板特殊化都不匹配因此会选择常规模板定义

局部特殊化的定义完全不同于常规模板的定义局部特殊化可以拥有一组与常规类模板完全不同的成员局部类模板特殊化的成员函数静态数据成员和嵌套类型必须有自己的定义这与类模板特殊化相同类模板成员的常规定义决不能用于实例化局部类模板特殊化的成员

类模板的局部模板特殊化构成了现代 C++ 用法中一些非常复杂的设计惯用语的基础如果您对此感兴趣可以阅读 Andrei Alexandrescu 撰写的《Modern C++ Design: Generic Programming and Design Patterns Applied》(AddisonWesley 年版)了解此用法的详细信息

函数模板特殊化

非成员函数模板也可以进行特殊化在有些情况下您可以充分利用有关类型的一些专门知识来编写比从模板实例化的函数更高效的函数在其他一些情况下常规模板的定义对某种类型而言根本就是错误的例如假设您拥有函数模板 max 的定义

template <class T>T max( T t T t ) {return ( t > t ? t :t );}

如果用 System::String 类型的模板参数实例化该函数模板所生成的实例就无法编译因为正如您在前面所看到的String 类不支持小于 (<) 或大于 (>) 运算符 中的代码说明了如何特殊化函数模板(同样必须先声明常规函数模板才能进行特殊化

如果可以从函数参数推断出模板参数则可以从显式特殊化声明中对实际类型参数省略函数模板名称的限定 max<String^>例如编译器可以在下面的 max 模板特殊化中推断出 T 绑定到 String因此在这种情况下为方便起见该语言允许使用下面的简写表示法

// 没问题从参数类型推断出 T 绑定到 Stringtemplate<> String^ max( String^ String^ );引入此显式特殊化后下面的调用就会解析为这个特殊化的实例 void foo( String^ s String^ s ) {String^ maxString = max( s s );     // }

如果两个参数的类型均为 String常规函数模板不会扩展这正是我们所需要的行为

只要提供了显式函数模板特殊化就必须始终指定 template<> 和函数参数列表例如max 的下面两个声明不合法并且在编译时会被标记为

// 错误无效的特殊化声明// 缺少 template<>String^ max<String^>( String^ String^ );// 缺少函数参数列表template<> String^ max<String^>;

有一种情况省略函数模板特殊化的 template<> 部分不是错误在您声明的普通函数带有与模板实例化相匹配的返回类型和参数列表的情况下

// 常规模板定义template <class T>T max( T t T t ) { /* */ }// 没问题普通函数声明!String^ max( String^ String^ );

毫无疑问您经常会感到很无奈并认为 C++ 真是太难理解了您可能想知道究竟为什么所有人都希望声明与模板实例化相匹配的普通函数而不希望声明显式特殊化那么请看下面的示例事情并不是完全按照您喜欢的方式进行的

void foo( String^ s String^ s ) {// 能否解析特殊化的实例?String^ maxString = max( muffy s );     // }

在 C++/CLI 下对于重载解决方案字符串文字的类型既是 const char[n] [其中 n 是文字的长度加一(用于终止空字符)]又是 System::String这意味着给定一组函数

void f( System::String^ );     // ()void f( const char* );         // ()void f( std::string );         // ()

如下所示的调用

// 在 C++/CLI 下解析为 ()f( bud not buddy );

与 () 完全匹配而在 ISOC++ 下解析结果会是 ()因此问题就是对于函数模板的类型推断而言字符串文字是否还是被当作 System::String 进行处理?简言之答案是(详细的答案将是我下一期专栏的主题该专栏将详细介绍函数模板)因此不选择 max 的特殊化 String 实例下面对 max 的调用

String^ maxString = max( muffy s ); // 错误

在编译时会失败因为 max 的定义要求两个参数的类型均为 T

template <class T> T max( T t T t );

那您能做些什么呢?像在下面的重新声明中一样将模板改为带有两个参数的实例

template <class Tclass T> ??? max( T t T t );

使我们能够编译带有 muffy 和 s 的 max 的调用但会因大于 (>) 运算符而断开并且指定要返回的参数类型

我想做的就是始终将字符串文字强制转换为 String 类型这也是挽救普通函数的方法

如果在推断模板参数时使用了某个参数那么只有一组有限的类型转换可用于将函数模板实例化的参数转换为相应的函数参数类型还有一种情况是显式特殊化函数模板正如您所看到的从字符串文字到 System::String 的转换不属于上述情况

在存在有害字符串文字的情况下显式特殊化无助于避免对类型转换的限制如果您希望不仅允许使用一组有限的类型转换则必须定义普通函数而不是函数模板特殊化这就是 C++ 允许重载非模板函数和模板函数的原因

我基本上已经讲完了不过还有最后一点需要说明创建一组您在图 中看到的 max 函数意味着什么?您知道调用

max( );

始终会解析为常规模板定义并将 T 推断为 int同样您现在还知道调用

max( muffy s );max( s muffy );

始终会解析为普通函数实例(其中文字字符串转换为 System::String)但是有一个问题调用

max( s s );

会解析为三个 max 函数中的哪一个?要回答此问题我们要查看解析重载函数的过程

重载函数的解析过程

解析重载函数的第一步是建立候选函数集候选函数集包含与被调用的函数同名并且在调用时能够看到其声明的函数

第一个可见函数是非模板实例我将该函数添加到候选列表中那么函数模板呢?在能够看到函数模板时如果使用函数调用参数可以实例化函数则该模板的实例化被视为候选函数在我的示例中函数参数为 s其类型为 String模板参数推断将 String 绑定到 T因此模板实例化 max(String^String^) 将添加到候选函数集中

只有在模板参数推断成功时函数模板实例化才会进入候选函数集但是如果模板参数推断失败不会出现错误函数实例化没有添加到候选函数集中

如果模板参数推断成功但是模板是为推断出的模板参数显式特殊化的(正如我的示例一样)会怎么样呢?结果是显式模板特殊化(而不是通过常规模板定义实例化的函数)将进入候选函数

因此此调用有两个候选函数特殊化的模板实例化和非模板实例

// 候选函数// 特殊化的模板template<> String^ max<String^>( String^ s String^ s );// 非模板实例String^ max( String^ String^ );

解析重载函数的下一步是从候选函数集中选择可行函数集对于要限定为可行函数的候选函数必须存在类型转换将每个实际参数类型转换为相应的形式参数类型在该示例中两个候选函数都是可行的

解析重载函数的最后一步是对参数所应用的类型转换进行分级以选择最好的可行函数例如两个函数看起来都很好既然两个函数都可行那么这是否应该被视为不明确的调用?

实际上调用是明确的将调用非模板 max因为它优先于模板实例化原因是在某种程度上显式实现的函数比通过常规模板创建的实例更为实用

令人吃惊的是在解决有害字符串文字的情况中我已经彻底消除了调用以前的 String 特殊化的可能性因此我可以消除这个问题我只需要常规模板声明以及重载的非模板实例

// 支持 String 的最终重载集template <class T>T max( T t T t ) { /* */ }String^ max( String^ String^ );

这不一定会很复杂但有一点是肯定的 在语言集成和灵活性方面它远远地超过了公共语言运行时 (CLR) 泛型功能可以支持的范围

模板特殊化是 C++ 模板设计的基础它提供了最好的性能克服了对单个或系列类类型的限制具有灵活的设计模式并且在实际代码中已证实其巨大价值在下一期专栏中我将深入分析 C++/CLI 对模板函数和常规函数的支持

请将您的疑问和意见通过 p 发送给 Stanley

Stanley B Lippman 是 Microsoft 公司 Visual C++ 团队的体系结构设计师他从 年开始在 Bell 实验室与 C++ 的设计者 Bjarne Stroustrup 一起研究 C++此后他在 Disney 和 DreamWorks 制作过动画还担任过 JPL 的高级顾问和 Fantasia 的软件技术主管

转到原英文页面

               

上一篇:初学者编程入门:学习C++的最大难度

下一篇:下一代C++:C++/CLI语言的属性探索