最近和一个朋友闲聊的时候他对我说一个人对C++的理解很多种境界,朋友不是个喜欢吹牛的人,于是听他细说,觉得很是有道理。
想写一篇C++ traits方面的文章已经有一段时间了,但是说实话traits这项技术确实有些晦涩,很担心写完了达不到期望的效果,于是每每试图以简炼的文字表达,慢慢的就等到了今天。
先说说我为什么专门对这项技术写一篇文章吧。记得当时在看STL/boost代码的时候经常遇到traits,当时惊叹于代码原来可以这样写,但是最初根本是看不懂的,查了一些资料才彻底理解了traits存在的意义。
本质定义:加上一层间接性,换来以定的灵活性。
看下面的代码:
struct is_void
{ static const bool value = false; };
template <>
struct is_void< void>
{ static const bool value = true; };
我们可以这样使用这份代码:
Is_void<false>::value 调用第一份代码,也就是说只要我们传入一个参数像下面这样:
Is_void<T>::value,其中T可以为任意类型,我们就可以判断这个类型是不是void在编译期。
完整测试代码如下:
struct is_void
{
static const bool value = false;
};
template <>
struct is_void< void>
{
static const bool value = true;
};
int _tmain( int argc, _TCHAR* argv[])
{
std::cout<<is_void< int>::value;
std::cout<<is_void< void>::value;
return 0;
}
摘自:http://www.cnblogs.com/pugang/archive/2012/10/17/2727378.html
在STL中为了提供通用的操作而又不损失效率,我们用到了一种特殊的技巧,叫traits编程技巧。具体的来说,traits就是通过定义一些结构体或类,并利用模板类特化和偏特化的能力,给类型赋予一些特性,这些特性根据类型的不同而异。在程序设计中可以使用这些traits来判断一个类型的一些特性,引发C++的函数重载机制,实现同一种操作因类型不同而异的效果。traits的编程技巧极度弥补了C++语言的不足 。
举例:
现在定义一个__type_traits可以获得类型的如下属性:
1. 是否存在non-trivial default constructor
2. 是否存在non-trivial copy constructor
3. 是否存在non-trivial assignment operator
4. 是否存在non-trivial destructor
struct __true_type {
};
struct __false_type {
};
template <class _Tp>
struct __type_traits {
typedef __false_type has_trivial_default_constructor;
typedef __false_type has_trivial_copy_constructor;
typedef __false_type has_trivial_assignment_operator;
typedef __false_type has_trivial_destructor;
};
问题:为什么把对象的所有的属性都定义为__false_type?
这样是采用最保守的做法,先把所有的对象属性都设置为__false_type,然后在针对每个基本数据类型设计特化的__type_traits,就可以达到预期的目的,如可以定义__type_traits<int>如下:
template <>
struct __type_traits<int> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
};
template <>
struct __type_traits<char> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
};
......
......
其他基本类型的traits也可以有相应的定义
__type_traits的偏特化版本
template <class _Tp>
struct __type_traits<_Tp*> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type;
};
我们可以自定义__type_traits的特化版本
比如对与自定义的Shape类型,我们可以这样定义__type_traits<Shape>
struct __type_traits<Shape> {
typedef __false_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type;
};
如果编译器够厉害,我们甚至可以不用自己去定义特化的__type_traits,编译器就能够帮我们搞定:)
如何使用呢?
假设现在用个模板函数fun需要根据类型T是否有non-trivial constructor来进行不同的操作,可以这样来实现:
template<class T>
void fun()
{
typedef typename __type_traits<T>::has_trivial_constructor _Trivial_constructor;
__fun(_Trivial_constructor()); // 根据得到的_Trivial_constructor来调用相应的函数
}
// 两个重载的函数
void _fun(_true_type)
{
cout<<"fun(_true_type)called"<<endl;
}
void _fun(_false_type)
{
cout<<"fun(_false_type) called"<<endl;
}
//测试代码
int main()
{
fun<char>();
fun<int>();
fun<char *>();
fun<double>();
}
摘自:http://www.cppblog.com/guojingjia2006/archive/2010/10/11/129405.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模板参数了。
不同的代码片段具有相同的结构,仅在实现细节方面有所不同,这是一种常见的情形。因此我们可以重用相同的部分,只对于有差别的那些细节,针对不同的需求进行实现。在C语言中,可以通过函数指针实现这一目标。比如C标准库中的qsort函数中那个函数指针(这句话在我看来真是相当风骚。。。以前只是无脑去用qsort,从来没有想过设计方面的事,不管你湿没湿,反正我湿了。。。读了linux的协议栈的部分代码之后,对这一点体会又有所加深。)或是C++里的虚函数。但这些方法增加了运行时的开销(我想这里应该是指函数指针和虚函数这两种方法,只有在运行时才能确定到底调用哪个函数)。
C++通过模板引入了泛型编程,搞定了运行时绑定这个问题,但是乍一看泛型这玩意似乎仍是无奈之举。毕竟同一个算法不可能在所有的数据结构上都有好的表现:比如,链表和数组排序方式是不同的,有序数据的查找也要快于无序数据。
于是traits来了。
Bjarne Stroustrup大神说:Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine "policy" or "implementation details".
trait是一类小对象,其目的是包含一些实现细节方面的信息,供其他对象和算法使用。
C和C++程序员一般会比较熟悉limits.h和float.h,这两个头文件决定了整型和浮点型的各种属性。C++程序员还会比较熟悉std::numeric_limits,乍一看这个类只是以不同的实现方式提供了与前两个头文件相同的功能,进一步解读这个类,可以发现traits的第一个优势:一致的接口。
如果使用limits.h和float.h,程序员必须记住类型的前缀(prefix)和特性(trait)。比如,DBL_MAX包含了double类型的最大值特性(我的理解是DBL即prefix,MAX就是trait)。但如果使用类似numeric_limits这样的traits类,就可以用numeric_limits<double>::max()来表示double类型的最大值。更为重要的是,甚至可以不必关心所要使用的类型。比如下面这个返回数组最大元素的简单模板函数:
1: template< class T >
2: T findMax(const T const * data, const size_t const numItems) {
3: // Obtain the minimum value for type T
4: T largest = std::numeric_limits< T >::min();
5: for(unsigned int i=0; i<numItems; ++i)
6: if (data[i] > largest)
7: largest = data[i];
8: return largest;
9: }
使用了numeric_limits之后,只要创建相应的特化模板,就可以把上面的模板扩展到任意的自定义类型。
下面说说如何创建自己需要的traits类。以boost的is_void trait为例。
首先定义一个实现默认行为的泛型模板。由于当具有类型时,都不是void,所以此时is_void::value应该为false,所以有:
1: template< typename T >
2: struct is_void{
3: static const bool value = false;
4: };
然后再对这个模板进行扩充,加入对void的特化。
1: template<>
2: struct is_void< void >{
3: static const bool value = true;
4: };
这样我们就有了一个完整的traits类型。用它可以判断任意一个作为模板参数传入的类型是否为void。
这回以boost::is_pointer为例。和刚才一样,先定义一个默认的模板。
1: template< typename T >
2: struct is_pointer{
3: static const bool value = false;
4: };
再对所有指针类型加入一个具体化的模板:
1: template< typename T >
2: struct is_pointer< T* >{
3: static const bool value = true;
4: };
接下来要考虑篇首提到的问题:如何用traits技术在编译时选择合适的算法?
通过下面的例子来说明。在下面的例子中,根据算法所操作的对象,在编译时选择是使用标准算法(即对象的类型不支持优化算法)还是优化算法(对象的类型支持优化算法)。
还是先创建一个默认的traits类,将其命名为supports_optimised_implementation,除了名字,这个traits类与is_void完全一样。即:
1: template<typename T>
2: struct supports_optimised_implementation{
3: static const bool value = false;
4: };
接下来在模板algorithm_selector中实现默认算法。在本例中,由于只在标准算法和优化算法之间进行选择,所以algorithm_selector使用bool类型进行参数化。如果要在更多算法之间进行选择的话,可以把模板参数类型换成int或者enum。这里当参数值为true时,表示使用优化算法。
1: template< bool b >
2: struct algorithm_selector {
3: template< typename T >
4: static void implementation( T& object )
5: {
6: //implement the alorithm operating on "object" here
7: }
8: };
1: template<>
2: struct algorithm_selector< true > {
3: template< typename T >
4: static void implementation( T& object ) {
5: object.optimised_implementation();
6: }
7: };
接下来给出供算法最终用户调用的泛型函数。注意,该函数调用algorithm_selector,而algorithm_selector用我们定义的supports_optimised_implementation traits类进行参数化。
1: template< typename T >
2: void algorithm( T& object ) {
3: algorithm_selector< supports_optimised_implementation< T >::value >::implementation(object);
4: }
假设现在有两个类ObjectA和ObjectB,A不支持优化算法,B支持。则有:
1: class ObjectB {
2: public:
3: void optimised_implementation() {
4: //...
5: }
6: };
还需要对supports_optimised_implementation加上针对B类的特化:
1: template<>
2: struct supports_optimised_implementation< ObjectB > {
3: static const bool value = true;
4: };
最后,当对模板进行实例化时:
1: int main(int argc, char* argv[]) {
2: ObjectA a;
3: algorithm( a );
4: // calls default implementation
5: ObjectB b;
6: algorithm( b );
7: // calls
8: // ObjectB::optimised_implementation();
9: return 0;
10: }
Over。
整理一下思路。实际上,用户调用的函数是algorithm,这个函数的用户唯一关心的是算法的功能。比如排序,用户希望,不管要排序的对象是什么类型,只要调用排序算法,就可以无差别实现各类对象的排序。因此,在调用算法时,肯定不希望函数的形式是类似algorithm(object, algorithm_type)这种,即用户自己还需要了解该对象适合使用哪种算法,如果是这样,用户还需要去了解对象的细节。因此在上面的例子中,在algorithm的内部进行了算法的选择。
转载自:http://www.cnblogs.com/youthlion/archive/2011/12/01/2255618.html