【翻譯】Traits:一種新的而且有用的Template技巧

ivkus發表於2022-06-02

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

相關文章