C++ traits学习笔记(二)

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的内部进行了算法的选择。


阅读更多
换一批

C++ Traits

08-17

今天看"modern c++ design"的时候发现自己竟然又把以前好不容易弄懂的Traits技术给忘记了,真是...又重新学习了一下,赶紧记下来。rnTraits技术可以用来获得一个 类型 的相关信息的。 首先假如有以下一个泛型的迭代器类,其中类型参数 T 为迭代器所指向的类型:rnrntemplate rnclass myIteratorrnrn ...rn;rnrn当我们使用myIterator时,怎样才能获知它所指向的元素的类型呢?我们可以为这个类加入一个内嵌类型,像这样:rntemplate rnclass myIteratorrnrn typedef T value_type; rn...rn;rn这样当我们使用myIterator类型时,可以通过 myIterator::value_type来获得相应的myIterator所指向的类型。rnrn现在我们来设计一个算法,使用这个信息。rntemplate rntypename myIterator::value_type Foo(myIterator i)rnrn ...rnrn这里我们定义了一个函数Foo,它的返回为为 参数i 所指向的类型,也就是T,那么我们为什么还要兴师动众的使用那个value_type呢? 那是因为,当我们希望修改Foo函数,使它能够适应所有类型的迭代器时,我们可以这样写:rntemplate //这里的I可以是任意类型的迭代器rntypename I::value_type Foo(I i)rnrn ...rnrn现在,任意定义了 value_type内嵌类型的迭代器都可以做为Foo的参数了,并且Foo的返回值的类型将与相应迭代器所指的元素的类型一致。至此一切问题似乎都已解决,我们并没有使用任何特殊的技术。然而当考虑到以下情况时,新的问题便显现出来了:rnrn原生指针也完全可以做为迭代器来使用,然而我们显然没有办法为原生指针添加一个value_type的内嵌类型,如此一来我们的Foo()函数就不能适用原生指针了,这不能不说是一大缺憾。那么有什么办法可以解决这个问题呢? 此时便是我们的主角:类型信息榨取机 Traits 登场的时候了rnrn....drum roll......rnrn我们可以不直接使用myIterator的value_type,而是通过另一个类来把这个信息提取出来:rntemplate rnclass Traitsrnrn typedef typename T::value_type value_type;rn;rn这样,我们可以通过 Traits::value_type 来获得myIterator的value_type,于是我们把Foo函数改写成:rntemplate //这里的I可以是任意类型的迭代器rntypename Traits::value_type Foo(I i)rnrn ...rnrn然而,即使这样,那个原生指针的问题仍然没有解决,因为Trait类一样没办法获得原生指针的相关信息。于是我们祭出C++的又一件利器--偏特化(partial specialization):rntemplate rnclass Traits //注意 这里针对原生指针进行了偏特化rnrn typedef typename T value_type;rn;rn通过上面这个 Traits的偏特化版本,我们陈述了这样一个事实:一个 T* 类型的指针所指向的元素的类型为 T。rnrn如此一来,我们的 Foo函数就完全可以适用于原生指针了。比如:rnint * p;rn....rnint i = Foo(p);rnTraits会自动推导出 p 所指元素的类型为 int,从而Foo正确返回。rnrnrn呼............ 终于写完了,开始写的时候没想到会写那么长,晕......

没有更多推荐了,返回首页