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