C++ traits学习笔记(一)
我的C++ traits学习笔记,这是第一篇。
刚刚接触traits技术,很多地方不知道理解的对还是不对,有路过的朋友如果发现问题请不吝指教,谢了!
翻译文章怪不容易的,转载请注明出处http://www.cnblogs.com/youthlion/archive/2011/12/01/2255618.html,再次感谢!
资料(a)
http://en.wikipedia.org/wiki/Traits_class
a traits class is a class template used to associate information or behaviour to a compile-time entity, typically a data type or a constant, without modifying the existing entity. In the C++ programming language, this is normally achieved by defining a primary class template, and creating explicit or partial specializations for the relevant types.
traits class是个类模板,在不修改一个实体(通常是数据类型或常量)的前提下,把属性和方法关联到一个编译时的实体。在c++中的具体实现方式是:首先定义一个类模板,然后进行显式特化或进行相关类型的部分特化。
我的理解是:traits是服务于泛型编程的,其目的是让模板更加通用,同时把一些细节向普通的模板用户隐藏起来。当用不同的类型去实例化一个模板时,不可避免有些类型会存在一些与众不同的属性,若考虑这些特性的话,可能会导致形成的模板不够“泛型”或是过于繁琐,而traits的作用是把这些特殊属性隐藏起来,从而实现让模板更加通用。
资料(b)
traits class技术最初似乎是Nathan C. Myers提出来的,这是他在1995年关于traits class的文章。草草地翻译一下:
问题描述:
ANSI/ISO C++标准库工作组那伙大牛在搞标准库对国际化的支持的时候遇到了问题。
比如,在iostream这个库里,steambuf的接口依赖于EOF,而EOF与所有字符类型都不同,在传统的库里,EOF为int,读取字符的那些函数的返回值也是整型:
1: class streambuf {
2: ...
3: int sgetc(); // return the next character, or EOF.
4: int sgetn(char*, int N); // get N characters.
5: };
如果把streambuf参数化为某种字符类型会咋样?此时我们不但需要一种针对字符的类型(type),还需要针对EOF的类型(type)。好吧,搞成这样好了:
1: template <class charT, class intT>
2: class basic_streambuf {
3: ...
4: intT sgetc();
5: int sgetn(charT*, int N);
6: };
iostream的用户们可不关心文件结束标志是个啥……更令人纠结的是当sgetc遇到文件末尾时该返回什么值?一定要有那个intT模板参数吗?那个额外的模板参数让你蛋疼了没?
Traits技术:
大牛们淡定地说,既然出了问题,索性咱就搞个新技术出来吧。。。这回咱不往咱原始的模板里添加乱七八糟的参数,咱干脆再定义一个模板。这个模板压根儿没打算让用户直接用,所以名字也可以稍微长一点,嗯。。也许400个单词的名字够说清这个模板的作用了。
1: template <class charT>
2: struct ios_char_traits { };
默认的traits class模板是个空模板。现在,对于真正的字符类型,咱就可以把这个模板特化一下,再加上一些有用的语义。
1: struct ios_char_traits<char> {
2: typedef char char_type;
3: typedef int int_type;
4: static inline int_type eof() { return EOF; }
5: };
哦,那位蹲着的同学发现了。。现在这个模板里面没有数据成员,而且只提供了public的定义(注意是struct而非class,默认访问权限,你懂的)。现在就开始重新定义streambuf模板:
1: template <class charT>
2: class basic_streambuf {
3: public:
4: typedef ios_char_traits<charT> traits_type;
5: typedef traits_type::int_type int_type;
6: int_type eof() { return traits_type::eof(); }
7: ...
8: int_type sgetc();
9: int sgetn(charT*, int N);
10: };
现在除了那些刺眼的typedef,这个模板和原来的也没啥太大区别,但是确实只剩一个用户感兴趣的模板参数了。编译器会去字符的traits class里查找关于字符类型的信息。
除了一些变量的声明方式有些不同,使用这个模板的用户代码也会和从前一样。
要在stream上使用一种新的字符类型,只要把ios_char_traits特化成这种新类型就可以了。现在小试一下,为宽字符提供支持:
1: struct ios_char_traits<wchar_t> {
2: typedef wchar_t char_type;
3: typedef wint_t int_type;
4: static inline int_type eof() { return WEOF; }
5: };
现在basic_streambuf模板和两个traits类的关系如下图:
字符串可以以同样的方式实现。
啥时候可以用traits class?目前为止现在我还没有什么心得,请大家自己体会作者原话:This technique turns out to be useful anywhere that a template must be applied to native types, or to any type for which you cannot add members as required for the template's operations.
另一个例子
下面咱来看看这种技术还可以用在什么地方。这个例子来自ANSI/ISO C++ [Draft] Standard(非最终版本)
假设正要写一个数值分析库,这个库处理float,double,long double这些数值类型。每种类型具有最大指数值和尾数所占的空间,还有“epsilon”函数(精度控制,如DBL_EPSILON)等。标准头文件<float.h>中定义了这些参数,但是一个参数化为数值类型的模板咋知道最大指数值是FLT_MAX_EXP还是DBL_MAX_EXP呢?
看看特化的traits模板如何搞定这个问题:
1: template <class numT>
2: struct float_traits { };
3:
4: struct float_traits<float> {
5: typedef float float_type;
6: enum { max_exponent = FLT_MAX_EXP };
7: static inline float_type epsilon() { return FLT_EPSILON; }
8: ...
9: };
10:
11: struct float_traits<double> {
12: typedef double float_type;
13: enum { max_exponent = DBL_MAX_EXP };
14: static inline float_type epsilon() { return DBL_EPSILON; }
15: ...
16: };
现在不管啥类型,模板的使用者都可以无差别使用max_exponent了。比如这儿有个矩阵类模板:
1: template <class numT>
2: class matrix {
3: public:
4: typedef numT num_type;
5: typedef float_traits<num_type> traits_type;
6: inline num_type epsilon() { return traits_type::epsilon(); }
7: ...
8: };
当我用double来实例化这个矩阵模板时,会有
1: matrix<double> somemat;
此时模板会被实例化为
1: class matrix {
2: public:
3: typedef double num_type;
4: typedef float_traits<num_type> traits_type; //此处num_type实际是double了
5: //这样又实例化了一个float_traits<double>,此时max_exponent为DBL_MAX_EXP,
6: //epsilon神马的也都是float_traits<double>所对应的
嗯,蹲着的那位同学又发现了。。现在所有的例子里面,每个模板都以public的方式提供了其参数对应的typedef,以及依赖这些typedef的常量(MAX_XXXX),返回值类型啥的(float_type)。没错:必须提供了相关类型的typedef,才能用这种类型去实例化模板。
关于默认模板参数
这个例子来自Stroustrup的《Design and Evolution of C++》359页,首先假设有个有点儿像traits的模板CMP:
1: template <class T> class CMP {
2: static bool eq(T a, T b) { return a == b; }
3: static bool lt(T a, T b) { return a < b; }
4: };
还有一个普通的字符串模板:
1: template <class charT> class basic_string;
现在可以定义一个对字符串进行比较的compare()函数:
1: template <class charT, class C = CMP<charT>>
2: int compare(const basic_string<charT>&,const basic_string<charT>&);
好,老少爷们儿们,请注意那个compare<>()的参数:首先,第二个参数C默认不是一个类,而是个实例化的类模板;其次,这个实例化的CMP模板的参数又是外层模板的第一个参数。OK,咱再绕回开始那个streambuf模板,看看出了些啥事儿:
1: template <class charT, class traits = ios_char_traits<charT> >
2: class basic_streambuf {
3: public:
4: typedef traits traits_type;
5: typedef traits_type::int_type int_type;
6: int_type eof() { return traits_type::eof(); }
7: ...
8: int_type sgetc();
9: int sgetn(charT*, int N);
10: };
这样一来,虽然模板又变成两个参数,但是通常情况下,模板的用户不需要关心第二个参数,有特殊需要时,又可以用特殊类型去特化那个traits。
运行时可变(runtime-variable)的traits
还可以进一步泛化。咱还没见着basic_streambuf的构造函数是吧?
1: template <class charT, class traits = ios_char_traits<charT> >
2: class basic_streambuf {
3: traits traits_; // member data
4: ...
5: public:
6: basic_streambuf(const traits& b = traits())
7: : traits_(b) { ... }
8:
9: int_type eof() { return traits_.eof(); }
10: };
通过添加构造函数的默认参数,我们在编译时和运行时都可以使用traits模板参数了。
posted on 2011-12-01 23:03 youthlion 阅读(16847) 评论(3) 编辑 收藏 举报