要想深入的理解 STL 的迭代器、分配器等,就必须了解 C++ 模板编程中的一个技巧—— Traits 。
1 、问题的提出
C++ 的模板特性为泛型编程提供了支持。这样我们就可以编写更加通用的代码,而不必过分去关心参数的类型。然而事实却是,类型的不同,很多时候却影响到了算法中的某个小小的实现。举个标准库里的类 string,wstring 。
其实它们对应的是两个模板,前者单字符,后者宽字符。
typedef basic_string<char, char_traits<char>, allocator<char> > string; typedef basic_string<wchar_t, char_traits<wchar_t>, allocator<wchar_t> > wstring;
模板 basic_string 需要有一个得出字符串长度的函数 length ,那么问题就来了。因为 char 和 wchar_t 所对应的求长度 API 并不一样。前者是 strlen, 后者是 wcslen 。
正是为了解决这样类似的问题, C++ 中的 traits 技巧被提炼出来了。
2 、解决方法
因为模板参数的类型不同,可能会影响到模板中具体的算法,那么我们就需要把这些与模板参数相差的方法从模板 basic _ string 中提取出来,而保证 basic _ string 算法的一致不受参数类型不同的影响。而上面的 char_traits 模板即是把与模板参数相差的方法都封装起来了。 如果定义这样一个模板.
template<class _Elem> struct char_traits { static void __CLRCALL_OR_CDECL assign(_Elem& _Left, const _Elem& _Right) { // assign an element _Left = _Right; } static bool __CLRCALL_OR_CDECL eq(const _Elem& _Left, const _Elem& _Right) { // test for element equality return (_Left == _Right); } //…… //…… //….. static size_t __CLRCALL_OR_CDECL length(const _Elem *_First) { // find length of null-terminated sequence // _DEBUG_POINTER(_First); size_t _Count; for (_Count = 0; !eq(*_First, _Elem()); ++_First) ++_Count; return (_Count); } };
这里的 legnth 实现是一个通胀算法循环遍历 , 并没有使用系统的 strlen,wcslen ,效率相对低一些。那么如果我一定要使用 strlen,wcslen 呢?
这里就需要用到模板的特化,也即指定模板的参数类型。
// STRUCT char_traits<wchar_t> template<> struct char_traits<wchar_t> { // properties of a string or stream wchar_t element static void __CLRCALL_OR_CDECL assign(_Elem& _Left, const _Elem& _Right) { // assign an element _Left = _Right; } static bool __CLRCALL_OR_CDECL eq(const _Elem& _Left, const _Elem& _Right) { // test for element equality return (_Left == _Right); } …… …… ….. static size_t __CLRCALL_OR_CDECL length(const _Elem *_First) { // find length of null-terminated sequence // _DEBUG_POINTER(_First); return (::wcslen(_First)); } }; // STRUCT char_traits<char> template<> struct char_traits<char> { // properties of a string or stream wchar_t element static void __CLRCALL_OR_CDECL assign(_Elem& _Left, const _Elem& _Right) { // assign an element _Left = _Right; } static bool __CLRCALL_OR_CDECL eq(const _Elem& _Left, const _Elem& _Right) { // test for element equality return (_Left == _Right); } …… …… ….. static size_t __CLRCALL_OR_CDECL length(const _Elem *_First) { // find length of null-terminated sequence // _DEBUG_POINTER(_First); return (::strlen(_First)); } };
当实现了上面两个特化的模板之后,在模板 basic_string 中,我们如果需要知道当前模板参数类型的字符串长度时,只需要调用 char_traits::length() 就可以调用到正确的函数了。
3 、总结
通过以上的事例,我们可以看出,具体的 traits 技巧非常简单。也就是将因为模板形参 ( 包括类型形参、非类型形参 ) 不同而导致的不同,抽取到新的模板中去,然后通过模板的特化 ( 全特化、偏特化均可,至少有一个模板形参不同即可 ) 来分别实现其不同。 这一类的模板,都会在命名中加上 traits 以示区别,所以也会把运用这一类方法称为 C++ 的 traits 技术。 traits 技术更展现出了一种编程的思想,也即将相同的提出复用,将不同的部分通过接口来实现。将模板形参与基不同的实现绑定在一起,其实与设计模式中的状态模式很相似,都体现出了相同的编程思想。只不过前者是编译时确定的,后者则是运行时确定的。
4 、注意
Boost 中有这样一个例子。
template< typename T > struct is_pointer{ static const bool value = false; }; template< typename T > struct is_pointer< T* >{ static const bool value = true; };
这样我就可以通过 is_pointer<T>::value 来判断当前类型是否为指针类型。
非类型模板形参
Template<bool b> Struct algo_sort { Template<typename T> Static void sort(T& obj) { Quick_sort(obj); } } Template<> Struct algo_sort<true> { Template<typename T> Static void sort(T& obj) { Select_sort(obj); } }
这样就能够模板形参调用不同的排序方法了 .
模板形参不仅仅与变量方法有关 , 还可能与类型有类 .
template< typename T > struct STRUCT_TYPE { typedef int MY_TYPE; typedef LONGLONG POWER_TYPE; }; template<> struct STRUCT_TYPE<double> { typedef float MY_TYPE; typedef double POW_TYPE; }; template< typename T > struct STRUCT_ALGO { // 下面的Typename是指示T::MY_TYPE是一个类型而不是成员变量 // 在VS2005中加与不加均可 typedef typename T::MY_TYPE myType; typedef T::POWER_TYPE powType; powType GetPow(const myType& value) { return value*value; } };
这样我们甚至可以将模板形参关联的变量类型也可以抽离出来 , 以提高模板的通用性 .