乐趣区

关于c++:翻译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<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.
退出移动版