文章来自Traits: a new and useful template technique
应该是很老(1995)的文章了,不过很适宜作为Template入门的资料。
ANSI/ISO C++规范库一开始就想反对国际化(internationalization),尽管一开始还没相好具体细节,然而最近5年逐步有点头绪了。当初失去的论断是,该当用template来对须要进行字符操作的工具进行参数化。
给现有的iostream和string类型进行参数化其实挺难的,须要创造一种新的技术才行。侥幸的是,这种技术能够很好的服用在其余中央。
问题
在iostream中,streambuf须要一个EOF来实现本身的性能。在个别的库中,EOF就是一个int而已;一些须要读取字符的函数也只返回int数值。
class streambuf { ... int sgetc(); // return the next character, or EOF. int sgetn(char*, int N); // get N characters. };
但如果用一种字符类型来参数化streambuf,会产生什么?一般来说,程序并不需要这个类型,而是须要这个类型的EOF的值。先试试看:
template <class charT, class intT> class basic_streambuf { ... intT sgetc(); int sgetn(charT*, int N); };
这个多进去的intT就有点烦人了。库的应用刚才不关怀所谓的EOF是啥。问题还不只这个,sgetc在遇到EOF时应该返回什么?莫非要再来一个模板参数?看来这种形式行不通。
Traits技法
这时候就须要引入Traits了。有了traits,就不须要从原来的模板里间接取参数,而是定义一个新的模板。用户个别不会间接应用这个新的模板,所以这个模板名字能够起的人性化一点。
template <class charT> struct ios_char_traits { };
默认的traits是个空类。对于实在的字符类型,能够特化这个模板以提供有意义的语义。
struct ios_char_traits<char> { typedef char char_type; typedef int int_type; static inline int_type eof() { return EOF; } };
ios_char_traits<char>并没有数据成员,只有一些定义。再看看streambuf该怎么定义。
template <class charT> class basic_streambuf { public: typedef ios_char_traits<charT> traits_type; typedef traits_type::int_type int_type; int_type eof() { return traits_type::eof(); } ... int_type sgetc(); int sgetn(charT*, int N); };
除去typedef,这个和最开始的定义很像。当初就有1个模板参数,也是用户须要关怀的惟一一个模板参数。编译器会从这个trait类中寻找须要的信息。除了一些变量的申明须要调整,用户的应用代码和之前看起来没有太大不同。
如果streambuf用了另外一个字符类型,这时只有从新特化ios_char_traits即可。要反对wchar_t能够这么写:
struct ios_char_traits<wchar_t> { typedef wchar_t char_type; typedef wint_t int_type; static inline int_type eof() { return WEOF; } };
string类能够用同样的形式参数化。
这个技巧的实用场景:1. 对原始类型进行模板参数化;2.对没方法改变的类进行定制
另一个例子
更进一步解释之前,再看看另一个例子(来自ANSI/ISO C++ [Draft] Standard)。
有一个数值计算库应用的类型有float, double和long double,每个类型都有关联的"epsilon"、指数和底数。这些数值在<float.h>里都有定义,但库中有一些工具不晓得改用FLT_MAX_EXP还是DBL_MAX_EXP。用traits技术能够很洁净的解决这个问题。
template <class numT> struct float_traits { }; struct float_traits<float> { typedef float float_type; enum { max_exponent = FLT_MAX_EXP }; static inline float_type epsilon() { return FLT_EPSILON; } ... }; struct float_traits<double> { typedef double float_type; enum { max_exponent = DBL_MAX_EXP }; static inline float_type epsilon() { return DBL_EPSILON; } ... };
当初能够在不晓得具体类型(float/double/long double)间接取到max_exponent。举个matrix的例子
template <class numT> class matrix { public: typedef numT num_type; typedef float_traits<num_type> traits_type; inline num_type epsilon() { return traits_type::epsilon(); } ... };
到当初为止的例子里,每个模板参数都有一系列public的typedef,而应用这些参数的类都强依赖于这些typedef。这绝非偶尔:大多数的状况下,作为参数的traits必须提供public的typedef,应用这些traits的template能力正确的实例化。
学到一点:肯定要提供这些public的typedef。
默认模板参数
到1993年为止,编译器就能够反对上述的用法。1993年11月后,一个更好的计划跃然纸上:能够制订默认的模板参数。当下曾经有不少编译器反对数值作为默认模板参数了,新计划更进一步,容许类型作为默认模板参数。
Stroustrup's Design and Evolution of C++ (page 359)有一个示例。首先定义一个traits: CMP,和一个简略的参数化的string。
template <class T> class CMP { static bool eq(T a, T b) { return a == b; } static bool lt(T a, T b) { return a < b; } }; template <class charT> class basic_string;
这时就能够为string自定义compare操作了:
template <class charT, class C = CMP<charT> > int compare(const basic_string<charT>&, const basic_string<charT>&);
这里不探讨具体实现细节,但须要关注第二个模板参数。首先,这个C不仅仅是class,而且是实例化后的class。其次,第二个模板参数(C)本人也须要参数,而须要的参数是compare的第一个模板参数(charT)。这在函数申明中是不能够的,但在模板申明时可行。
这种形式容许用户能够自定义比拟的过程。把这个技术利用在咱们本人的streambuf模板上看下:
template <class charT, class traits = ios_char_traits<charT> > class basic_streambuf { public: typedef traits traits_type; typedef traits_type::int_type int_type; int_type eof() { return traits_type::eof(); } ... int_type sgetc(); int sgetn(charT*, int N); };
这给了咱们为特定char定制traits的机会。这对库的用户来说很重要,因为EOF在不同的字符集映射中是有可能不一样的。
运行时的Traits
更进一步,看戏streambuf的构造函数:
template <class charT, class traits = ios_char_traits<charT> > class basic_streambuf { traits traits_; // member data ... public: basic_streambuf(const traits& b = traits()) : traits_(b) { ... } int_type eof() { return traits_.eof(); } };
当初咱们traits也能够在运行时发挥作用了,而不仅仅是编译时。在这个例子中,traits_.eof()可能是一个动态函数,或者是一个一般的成员函数。如果是一般成员函数,eof()可能会用到结构traits时的一些参数。(这个技巧是有理论应用场景的,比方规范库里的容器都有的allocator)
值得注意的是,对应用方来说,当初没有任何的扭转,默认参数能够满足大部分的应用需要。然而当有本人非凡需要时,当初的模板定义也能提供批改的机会。不论什么状况下,咱们都会生成最优的代码,如果不须要额定的代价,咱们就不会引入这些额定的代价!
总结
只有编译器反对template,traits技巧就能够间接上手用起来了。
Traits能够将相关联的类型、值、函数等用模板参数关联起来,同时不引入过多的噪声。这项语言个性(默认模板参数)极大的裁减了语言能力,提供了足够的灵活性,也丝毫不侵害运行效率。
参考
- Stroustrup, B. Design and Evolution of C++, Addison-Wesley, Reading, MA, 1993.
- Barton, J.J., and L.R. Nackman, Scientific and Engineering C++, Addison-Wesley, Reading, MA, 1994.
- Veldhuizen, T. " Expression Templates", C++ Report, June 1995, SIGS Publications, New York.