【翻译】Traits:一种新的而且有用的Template技巧

文章来自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并没有数据成员,只有一些定义。再看看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.