Traits 技术(转)

来源:互联网 发布:哪个软件可以文件翻译 编辑:程序博客网 时间:2024/06/05 18:09

什么是traits,为什么人们把它认为是C++ Generic Programming的重要技术?
简洁地说,traits如此重要,是因为此项技术允许系统在编译时根据类型作一些决断,就好像在运行时根据值来做出决断一样。更进一步,此技术遵循“另增一个间接层”的谚语,解决了不少软件工程问题,traits使您能根据其产生的背景(context) 来做出抉择。这样最终的代码就变得清晰易读,容易维护。如果你正确运用了traits技术,你就能在不付出任何性能和安全代价的同时得到这些好处,或者能够契合其他解决方案上的需求。
先举个浅显易懂的例子来说明traits的用法:
//首先假如有以下一个泛型的迭代器类,其中类型参数 T 为迭代器所指向的类型:
template <typename T>
class myIterator
{
...
};
那么当使用myIterator时,怎样才能知道它所指向元素的类型呢?一种解决方案是为这个类加入一个内嵌类型:
template <typename T>
class myIterator
{
typedef T value_type;
...
};
当使用myIterator时,可以通过myIterator::value_type来获得相应的myIterator所指向的类型。
template <typename T>
typename myIterator<T>::value_type func(myIterator<T> i)
{
...
}
这里定义了一个函数func,返回值类型为参数i所指的类型,也就是模板参数T,那么为什么不直接使用模板参数T,而要绕着圈去使用那个value_type呢?其原因是当修改func函数时,它能够适应所有类型的迭代器,如下所示:
template <typename I> //这里的I可以是任意类型的迭代器
typename I::value_type func(I i)
{
...
}
现在,任意定义了value_type内嵌类型的迭代器都可以做为func的参数了,并且func的返回值的类型将与相应迭代器所指的元素的类型一致。至此一切问题似乎都已解决,并且似乎并没有使用任何特殊的技术。然而当考虑到以下情况时,新的问题便显现出来了:
原生指针也完全可以做为迭代器来使用,然而显然没有办法为原生指针添加一个value_type的内嵌类型,如此一来func()函数就不能适用原生指针了,这不能不说是一大缺憾。那么有什么办法可以解决这个问题呢?此时不禁想到了用Traits萃取类型信息。
可以不直接使用myIterator的value_type,而是通过另一个类来把这个信息提取出来:
template <typename T>
class Traits
{
typedef typename T::value_type value_type;
};
这样以后就可以通过Traits<myIterator>::value_type来获得myIterator的value_type,于是func函数改写成:
template <typename I> //这里的I可以是任意类型的迭代器
typename Traits<I>::value_type Foo(I i)
{
...
}
然而,即使这样,那个原生指针的问题仍然没有解决,因为Trait类还是没办法获得原生指针的相关信息。于是不妨将Traits偏特化(partial specialization):
template <typename T>
class Traits<T*> //注意 这里针对原生指针进行了偏特化
{
typedef typename T value_type;
};
通过上面这个Traits的偏特化版本,一个T*类型的指针所指向的元素的类型为T。如此一来,我们func函数就完全可以适用于原生指针了。比如:
int * p;
....
int i = func(p);
Traits会自动推导出p所指元素的类型为int,从而func正确返回。
 
现在再看一个更加一般的例子——smart pointers。
假设你正在设计一个SmartPtr模板类,对于一个smart pointer 来说,它的最大的用处是可以自动管理内存问题,同时在其他方面又像一个常规指针。但是有些C++的Smart pointer实现技术却非常令人难以理解。这一残酷的事实带来了一个重要实践经验:你最好尽一切可能一劳永逸,写出一个出色的、具有工业强度的 smart pointer来满足你所有的需求。此外,你通常不能修改一个类来适应你的smart pointer,所以你的SmartPtr一定要足够灵活。
有不少类层次使用引用计数(reference counting)以及相应的函数管理对象的生存期。然而,并没有reference counting的标准实现方法,每一个C++库的供应商在实现的语法和/或语义上都有所不同。例如,在你的应用程序中有这样两个interfaces:
大部分的类实现了RefCounted接口:
class RefCounted
{
public:
virtual void IncRef() = 0;
virtual bool DecRef() = 0;
// if you DecRef() to zero references, the object is destroyed
// automatically and DecRef() returns true
virtual ~RefCounted() {}
};
而由第三方提供的Widget类使用不同的接口:
class Widget
{
public:
void AddReference();
int RemoveReference();
// returns the remaining number of references; it's the client's
// responsibility to destroy the object
...
};
不过你并不想维护两个smart pointer类,你想让两种类共享一个SmartPtr。一个基于traits的解决方案把两种不同的接口用语法和语义上统一的接口包装起来,建立针对普通类的通用模板,而针对Widget建立一个特殊化版本,如下:
template <class T>
class RefCountingTraits
{
static void Refer(T* p)
{
p->IncRef(); // assume RefCounted interface
}
static void Unrefer(T* p)
{
p->DecRef(); //assume RefCounted interface
}
};
template<>
class RefCountingTraits<Widget>
{
static void Refer(Widget* p)
{
p->AddReference(); //use Widget interface
}
static void Unrefer(Widget* p)
{
//use Widget interface
If (p->RemoveReference() == 0)
delete p;
}
};
在SmartPtr里,我们像这样使用RefCountingTraits:
template <class T>
class SmartPtr
{
private:
typedef RefCountingTraits<T> RCTraits;
T* pointee_;
public:
...
~SmartPtr()
{
RCTraits::Unrefer(pointee_);
}
};
当然在上面的例子里,你可能会争论说你可以直接特殊化Widget类的SmartPtr的构造与析构函数。你可以使用把模板特殊化技术用在 SmartPtr本身,而不是用在traits上头,这样还可以消除额外的类。尽管对这个问题来说这种想法没错,但还是由一些你需要注意的缺陷:
⑴这么干缺乏可扩展性。如果给SmartPtr再增加一个模板参数,你不能特殊化这样一个SmartPtr<T. U>,其中模板参数T是Widget,而U可以为其他任何类型。
⑵最终代码不那么清晰。Trait有一个名字,而且把相关的东西很好的组织起来,因此使用traits的代码更加容易理解。相比之下,用直接特殊化SmartPtr成员函数的代码,看上去更招黑客的喜欢。
用继承机制的解决方案,就算本身完美无瑕,也至少存在上述的缺陷。解决这样一个变体问题,使用继承实在是太笨重了。此外,通常用以取代继承方案的另一种经典机制——containment,用在这里也显得画蛇添足,繁琐不堪。相反,traits方案干净利落,简明有效,物合其用,恰到好处。
Traits的一个重要的应用是“interface glue”(接口胶合剂),通用的、可适应性极强的适配子。如果不同的类对于一个给定的概念有着不同的实现,traits可以把这些实现再组织统一成一个公共的接口。
对于一个给定类型提供多种TRAITS:现在,我们假设所有的人都很喜欢你的SmartPtr模板类,直到有一天,在你的多线程应用程序里开始现了神秘的bug。你发现罪魁祸首是Widget,它的引用计数函数并不是线程安全的。现在你不得不亲自实现Widget:: AddReference和Widget::RemoveReference,最合理的位置应该是在RefCountingTraits中,打上个补丁吧:
// Example 7: Patching Widget's traits for thread safety
template <>
class RefCountingTraits<Widget>
{
static void Refer(Widget* p)
{
Sentry s(lock_); // serialize access
p->AddReference();
}
static void Unrefer(Widget* p)
{
Sentry s(lock_); // serialize access
if (p->RemoveReference() == 0)
delete p;
}
private:
static Lock lock_;
};
不幸的是,虽然你重新编译、测试之后正确运行,但是程序慢得像蜗牛。仔细分析之后发现,你刚才的所作所为往程序里塞了一个糟糕的瓶颈。实际上只有少数几个Widget是需要能够被好几个线程访问的,余下的绝大多数Widget都是只被一个线程访问的。你要做的是告诉编译器按你的需求分别使用多线程traits和单线程traits这两个不同版本。你的代码主要使用单线程traits.
如何告诉编译器使用哪个traits?一种方法是把traits作为另一个模板参数传给SmartPtr。缺省情况下传递老式的traits模板,而用特定的类型实例化特定的模板。
template <class T, class RCTraits = RefCountingTraits<T> >
class SmartPtr
{
...
};
你对单线程版的RefCountingTraits<Widget>不做改动,而把多线程版放在一个单独的类中:
class MtRefCountingTraits
{
static void Refer(Widget* p)
{
Sentry s(lock_); // serialize access
p->AddReference();
}
static void Unrefer(Widget* p)
{
Sentry s(lock_); // serialize access
if (p->RemoveReference() == 0)
delete p;
}
private:
static Lock lock_;
};
现在你可将SmartPtr<Widget>用于单线程目的,将SmartPtr<Widget,MtRefCountingTraits>用于多线程目的。
最后,以SGI STL中的__type_traits结束本篇讨论,在SGI 实现版的STL中,为了获取高效率,提供了__type_traits,用来提取类的信息,比如类是否拥有trival的构造、析构、拷贝、赋值操作,然后跟据具体的信息,就可提供最有效率的操作。以下摘录cygwin的gcc3.3源码,有改动,在<type_traits.h>中。
struct __true_type {};
struct __false_type {};
template <class _Tp>
struct __type_traits
{
typedef __true_type    this_dummy_member_must_be_first;
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;
typedef __false_type    is_POD_type;
};
对于普通类来讲,为了安全起见,都认为它们拥有non-trival的构造、析构、拷贝、赋值函数,POD是指plain old data。接下来对C++的原生类型(bool,int, double之类)定义了显式的特化实现,以double为例:
template<>
struct __type_traits<long double> {
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;
};
还有,对所有的原生指针来讲,它们的构造、析构等操作也是trival的,因此有:
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;
};
简化<stl_algobase.h>中copy的部分代码来说明对__type_traits的应用。
template<typename _Tp>
inline _Tp* __copy_trivial(const _Tp* __first, const _Tp* __last, _Tp* __result)
{
memmove(__result, __first, sizeof(_Tp) * (__last - __first));
return __result + (__last - __first);
}
template<typename _Tp>
inline _Tp* __copy_aux (_Tp* __first, _Tp* __last, _Tp* __result, __true_type)
{ return __copy_trivial(__first, __last, __result); }
template<typename _Tp>
inline _Tp* __copy_aux (_Tp* __first, _Tp* __last, _Tp* __result, __false_type)
{ 另外处理;}
template<typename _InputIter, typename _OutputIter> inline
_OutputIter copy (_InputIter __first, _InputIter __last, _OutputIter __result)
{
typedef typename iterator_traits<_InputIter>::value_type _ValueType;
typedef typename __type_traits<_ValueType>::has_trivial_assignment_operator _Trivial;
return __copy_aux(__first, __last, __result, _Trivial());
}
Copy 函数利用__type_traits判断当前的value_type是否有trival的赋值操作,如果是,则产生类__true_type的实例,编译 时选择__copy_trivial函数进行memmove,效率最高。如果是non-trival的赋值操作,则另作处理,效率自然低些。__true_type和__false_type之所以是类,就因为C++的函数重载是根据类型信息来的,不能依据参数值来判别。使用SGI STL时,可以为自己的类定义__type_traits显式特化版本,以求达到高效率。


注:

 我可以这样理解否:Trait实际上是对逻辑上处于相同地位的类和数据类型再进行抽象和封装呢??