STL中的Traits技法记录

    xiaoxiao2023-12-01  171

    看过《STL源码剖析》的人,一定会知道里面讲到的Traits编程技法。刚开始看到书上这部分的介绍的时候,是有点不太理解这个技法的作用的,但是经过网上找资料了解之后,并且反复看了书上的内容之后,才渐渐了解到这个技法的重要性,对于迭代器及泛型思维的重要性。

    一句话简述这个技法的主要作用,就是:可以通过迭代器的某个操作,直接获取迭代器里面保存的容器的元素类型。如果没有Traits技法,是无法实现这个操作的。要深刻了解Traits技法,依次理解下面的概念就可以了。

    1. 迭代器相应型别(associated types)

           什么是迭代器的相应识别,实际上就是指迭代器所指物(关联的容器的元素)的类型。因为C++中并不支持获取一个对象的类型的操作,更何况是将其类型作为一个变量来获取。在STL中,使用了function template的参数推导(argument deduction)机制来实现这个操作(一下源码来自《STL源码剖析》一书)。

    template <class I, class T> void func_impl(I iter, T t) { T tmp; // 这里解决了问题。T就是迭代器所指之物的型别,本例为int // ... 这里做原本func()应该做的工作 } template <class I> inline void func(I iter) { func_impl(iter, *iter); // func的工作全部移往func_impl } int main() { int i; func(&i); }

          正常情况下,如果直接将迭代器对象传给函数,那我们只能得到迭代器的类型。而迭代器重载了*操作符,我们就可以通过迭代器解引用+参数推导得到迭代器里元素的类型了。上面的func_impl相当于起了适配器的作用。

          那,如果我们要获取返回值的类型呢?

    2. 内嵌型别(nested type)

          参数的类型,我们可以通过参数推导得到,那返回值的类型怎么获得呢,毕竟大多数情况下我们对迭代器的操作,返回值的类型应该是元素的类型。而上面的方法,我们无法在定义func函数的时候就得到其返回值的类型。这个时候我们就可以声明内嵌型别了。一下代码也来自该书中。

    template <class T> struct MyIter { typedef T value_type; // 内嵌型别声明 T *ptr; MyIter(T* p=0) : ptr(p) {} T& operator*() const { return *ptr; } // ... } template <class I> typename I::value_type // 这一整行是func的返回值型别 func(I iter) { return *iter; } // ... MyIter<int> ite(new int(8)); cout << func(ite);

          可以看到,在MyIter迭代器类的定义中,使用内嵌型别声明,将迭代器保存元素类型用typedef关键字起了别名value_type,然后在func函数中,就可以直接通过获取迭代器类中的value_type来获得其保存元素的类型作为函数的返回值了。这里为什么要用typename关键字,直接引用书中原文:“因为T是一个template参数,在它被编译器具现化之前,编译器对T一无所悉,换句话说,编译器此时并不知道MyIter<T>::value_type代表的是一个型别或是一个member function或是一个data member。关键词typename的用意在于告诉编译器这是一个型别,如此才能顺利通过编译”。

          通过这种方式我们可以得到迭代器保存元素的指针型别、引用型别等。但是,这种方式只能用在自定义的迭代器类中,因为其是class type。但对于原生指针呢?比如int*、char*等,因为我们并无法为其定义内嵌型别。而这个时候,就要针对其进行特殊处理了。

    3. 偏特化(partial specialization)

          一般来说,我们声明一个模板类的时候,所有的template参数都是使用者在创建特定对象的时候传入的。而偏特化,就是让我们在这样子的泛化设计中,提供一个特化的版本,针对某个template参数进行指定,这样子当使用者传入该template参数和我们指定的参数一直的时候,编译器将使用这个特化的模板类创建对象,而不会使用泛化的版本进行创建,此外还有一种是全特化。

    // 泛化 template<typename U, typename V, typename T> class C {...}; // 偏特化 template<typename U, typename V> class C<U, V, int> {...}; // 全特化 template<> class C<int, char, int> {...}; 创建对象时: C<int, string, char> c1; // 使用泛化模板类 C<int, string, int> c2; // 使用偏特化模板类 C<int, char, int> c3; // 使用全特化模板类

          这里补上《STL源码剖析》中的一段话:”‘所谓partial specialization的意思是提供另一份template定义式,而其本身仍是templatized’。《泛型思维》一书对partial specialization的定义是:‘针对(任何)template参数更进一步的条件限制所设计出来的一个特化版本’“。结合上面的例子,基本上就能理解偏特化的作用了。

          认识了偏特化,我们就针对原生指针做返回值类型的特殊化处理了。

    4. 特性萃取

          直接上代码。

    template <class T> struct iterator_traits { // traits意为“特性” typedef typename T:value_type value_type; } template<class T> struct iterator_traits<T*> { typedef T value_type; }

           有了前面的概念,我们很容易理解当创建iterator_traits对象时,如果模板参数传入的是非指针类型(一般自定的迭代器都是非指针的),则调用第一个版本创建对象,这时value_type为保存的元素的类型;而若传入的是指针类型,则调用第二个版本创建对象,此时value_type则是指针保存元素的类型。

           通过这种方式,我们就可以很容易的区分出原生指针即自定义的迭代器,正确的取出元素的类型。这个时候,上面func的代码可以写成:

    template <class I> typename iterator_traits<I>::value_type func(I ite) { return *ite; }

          这样不管传入的是原生指针还是自定义迭代器类,都能正常工作了。

          正是靠着Traits编程技法,STL实现了很好的泛化,因为可以视为所有元素的类型都是统一的。即使是自定义的类及迭代器,只要提供了指定的操作及内嵌型别,就可以使用STL中所有已有的函数。

     

    补充:

          实际上在STL中,不止一种相应型别,有五种相应型别(迭代器移动特性、迭代器所指对象型别、迭代器之间的距离、迭代器所指之物的内容是否允许改变、迭代器所指对象指针型别):

    template <class I> struct iterator_traits { typedef typename I::iterator_category iterator_category; typedef typename I::iterator_value_type value_type; typedef typename I::difference_type difference_type; typedef typename I::pointer pointer_type; typedef typename I::reference reference; }

         

    最新回复(0)