概述:
traits是一种特性萃取技术,它在Generic Programming中被广泛运用,常常被用于使不同的类型可以用于相同的操作,或者针对不同类型提供不同的实现.traits在实现过程中往往需要用到以下三种C++的基本特性:
enum
typedef
template (partial) specialization
其中:
enum用于将在不同类型间变化的标示统一成一个,它在C++中常常被用于在类中替代define,你可以称enum为类中的define;
typedef则用于定义你的模板类支持特性的形式,你的模板类必须以某种形式支持某一特性,否则类型萃取器traits将无法正常工作.看到这里你可能会想,太苛刻了吧?其实不然,不支持某种特性本身也是一种支持的方式(见示例2,我们定义了两种标示,__xtrue_type和__xfalse_type,分别表示对某特性支持和不支持).
template (partial) specialization被用于提供针对特定类型的正确的或更合适的版本.
借助以上几种简单技术,我们可以利用traits提取类中定义的特性,并根据不同的特性提供不同的实现.你可以将从特性的定义到萃取,再到traits的实际使用统称为traits技术,但这种定义使得traits显得过于复杂,我更愿意将traits的定义限于特性萃取,因为这种定义使得traits显得更简单,更易于理解,^_^.
举例:
上面提到过,traits可被用于针对不同类型提供不同的实现,那么下面就举两个例子来说明如何实现这一点.
Example 1:
假定我们需要为某个类设计一个可以对所有类型(包括普通的int/long...,提供了clone方法的复杂类型CComplexObject,及由该类派生的类)进行操作的函数clone,下面,先用OO的方法来考虑一下解决方案.看到前面的条件,最先跳进你脑子里的肯定是Interface,pure virtual function等等.对于我们自己设计的类CComplexObject而言,这不是问题,但是,对于基本数据类型呢?还有那些没有提供clone方法的复杂类型呢?(这时候你可能会想,要是Java该多easy,所有类都默认从Object派生,而Object已提供了一个默认的clone方法,但是,要使类真正支持clone,还必须implements Cloneable,所以,同样也不能避免这里遇到的麻烦).
下面是一个可能的解决方案:
template <typename T, bool isClonable>
class XContainer
{
...
void clone(T* pObj)
{
if (isClonable)
{
pObj->clone();
}
else
{
//... non-Clonable algorithm ...
}
}
};
但是只要你测试一下,这段代码不能通过编译.为什么会这样呢?原因很简单:对于没有实现clone方法的非Clonable类或基本类型,pObj->clone这一句是非法的.
那么怎样解决上面的这个难题呢?上面不能通过编译的代码告诉我们,要使我们的代码通过编译,就不能使非Clonable类或基本类型的代码中出现pObj->clone,即我们需要针对不同类型提供不同的实现.为了实现这一点,我们可以在我们的模板类中用enum定义一个trait,以标示类是否为Clonable类,然后在原模板类内部引入一个traits提取类Traits,通过对该类进行specilizing,以根据不同的trait提供不同的实现.具体实现如下:
#include <iostream>
using namespace std;
class CComplexObject // a demo class
{
public:
void clone() { cout << "in clone" << endl; }
};
// Solving the problem of choosing method to call by inner traits class
template <typename T, bool isClonable>
class XContainer
{
public:
enum {Clonable = isClonable};
void clone(T* pObj)
{
Traits<isClonable>().clone(pObj);
}
template <bool flag>
class Traits
{
};
template <>
class Traits<true>
{
public:
void clone(T* pObj)
{
cout << "before cloning Clonable type" << endl;
pObj->clone();
cout << "after cloning Clonable type" << endl;
}
};
template <>
class Traits<false>
{
public:
void clone(T* pObj)
{
cout << "cloning non Clonable type" << endl;
}
};
};
void main()
{
int* p1 = 0;
CComplexObject* p2 = 0;
XContainer<int, false> n1;
XContainer<CComplexObject, true> n2;
n1.clone(p1);
n2.clone(p2);
}
编译运行一下,上面的程序输出如下的结果:
doing something non Clonable
before doing something Clonable
in clone
after doing something Clonable
这说明,我们成功地根据传入的isClonable模板参数为模板实例选择了不同的操作,在保证接口相同的情况下,为不同类型提供了不同的实现.
Example 2:
我们再对上面的例子进行一些限制,假设我们的clone操作只涉及基本类型和CComplexObject及其派生类,那么我们可以进一步给出下面的解法:
#include <iostream>
using namespace std;
struct __xtrue_type { }; // define two mark-type
struct __xfalse_type { };
class CComplexObject // a demo class
{
public:
virtual void clone() { cout << "in clone" << endl; }
};
class CDerivedComplexObject : public CComplexObject // a demo derived class
{
public:
virtual void clone() { cout << "in derived clone" << endl; }
};
// A general edtion of Traits
template <typename T>
struct Traits
{
typedef __xfalse_type has_clone_method; // trait 1: has clone method or not? All types defaultly has no clone method.
};
// Specialized edtion for ComplexObject
template <>
struct Traits<CComplexObject>
{
typedef __xtrue_type has_clone_method;
};
template <typename T>
class XContainer
{
template <typename flag>
class Impl
{
};
template <>
class Impl <__xtrue_type>
{
public:
void clone(T* pObj)
{
pObj->clone();
}
};
template <>
class Impl <__xfalse_type>
{
public:
void clone(T* pObj)
{
}
};
public:
void clone(T* pObj)
{
Impl<Traits<T>::has_clone_method>().clone(pObj);
}
};
void main()
{
int* p1 = 0;
CComplexObject c2;
CComplexObject* p2 = &c2;
CDerivedComplexObject c3;
CComplexObject* p3 = &c3; // you must point to a derived object by a base-class pointer,
//it's a little problem
XContainer<int> n1;
XContainer<CComplexObject> n2;
XContainer<CComplexObject> n3;
n1.clone(p1);
n2.clone(p2);
n3.clone(p3);
}
现在,所有基本类型以及CComplexObject类系都可以用于XContainer了.
结语:
看到这里,你或许会说,traits不过如此,还以为是什么高深的玩意呢!其实技术就是这样,说白了都很Easy,关键是怎么将他们用于实际,为实际的Designing/Development服务.毕竟,在IT领域,不能应用于实际的技术是没有价值的.
C++ traits学习笔记(二)
这是我学习traits翻译的第二篇文章,原文地址:http://accu.org/index.php/journals/442,有不当的地方请过路的朋友指教,谢谢!
翻译文章也怪不容易的,转载请注明出处,感谢!
不同的代码片段具有相同的结构,仅在实现细节方面有所不同,这是一种常见的情形。因此我们可以重用相同的部分,只对于有差别的那些细节,针对不同的需求进行实现。在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的内部进行了算法的选择。