文章来自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.