Fork me on GitHub

读书笔记 effective c++ Item 47 使用traits class表示类型信息

STL主要由为容器,迭代器和算法创建的模板组成,但是也有一些功能模板。其中之一叫做advance。Advance将一个指定的迭代器移动指定的距离:

1 template<typename IterT, typename DistT> // move iter d units
2 void advance(IterT& iter, DistT d); // forward; if d < 0,
3 // move iter backward

 

从概念上来说,advance仅仅做了iter += d,但是advance并不是用这种方式实现的,因为只有随机访问迭代器支持+=操作。其他一些更加弱的迭代器类型必须通过反复的++或者--d次来实现advance。

1. 五种迭代器种类回顾

你不记得STL迭代器的种类了?没问题,我们现在做一些简单的回顾。总共有5类迭代器,对应着它们支持的五种操作。输入迭代器(input iterator只能向前移动,一次只能移动一步,只能读取它们指向的内容,并且只能读一次。这种迭代器模拟的是输入文件的读指针;C++库的istream_iterator是这种迭代器的代表。输出迭代器(output iterator同输入迭代器类似,但是对于输出迭代器来说:它们只能向前移动,每次只能移动一步,只能对它们指向的内存进行写操作,并且只能写一次。它们模拟的是输出文件的写指针;ostream_iterator代表了这种迭代器。这是两类功能最弱的迭代器。因为输入和输出迭代器只能向前移动,只能对它们所指向的内容进行读写最多一次,它们只能为one-pass算法所使用。

一个更加强大的迭代器种类由前向迭代器(forward iterator组成。这种迭代器能做输入和输出迭代器能做的所有事情,并且它们能对它们指向的内容进行多次的读写。这就使得它们能被multi-pass算法所使用。STL没有提供单链表,但是一些库却提供了(通常叫做slist),这种容器中的迭代器为前向迭代器。TR1中的哈希容器(Item 54)迭代器也可能是前向迭代器。

双向迭代器(bidirectional iterators和前向迭代器相比添加了向后移动的能力。为STL中的list提供的迭代器就属于这种类别,为set,multiset,map和multimap提供的迭代器也是这种类别。

最强大的迭代器类别叫做随机访问迭代器(random access iterator。这种类型的迭代器和双向迭代器相比添加了执行“迭代器运算(iterator arithmetic)”的能力,也就是在常量时间内向前或者向后跳跃任意的距离。这种运算同指针运算类似,不要吃惊,因为随机访问迭代器模拟的就是内建类型的指针,内建类型指针的行为表现就如同随机访问迭代器。Vector,deque和string迭代器都是随机访问迭代器。

为了识别这五种迭代器类型,C++在标准库中为五种迭代器类型提供了一个“tag结构体”:

1 struct input_iterator_tag {};
2 struct output_iterator_tag {};
3 struct forward_iterator_tag: public input_iterator_tag {};
4 struct bidirectional_iterator_tag: public forward_iterator_tag {};
5 struct random_access_iterator_tag: public bidirectional_iterator_tag {};

 

这些结构体之间的继承关系是有效的“is-a”关系(Item32):所有的前向迭代器同样是输入迭代器,等等。我们很快会看到这种继承的效用。

2. 如何实现advance简析

回到advance。考虑到不同的迭代器功能,实现advance的一种方法是使用循环的最小公分母策略:对迭代器进行反复加或者减。然而,这个方法会花费线性的时间。随机访问迭代器支持常量时间的迭代器算法,在我们需要的时候会使用它的这种能力。

我们真正想要的是像下面这样去实现advance:

 1 template<typename IterT, typename DistT>
 2 void advance(IterT& iter, DistT d)
 3 {
 4 if (iter is a random access iterator) {
 5 
 6 iter += d;                           // use iterator arithmetic
 7 
 8 }                                        // for random access iters
 9 
10 else {                                
11 
12 
13 if (d >= 0) { while (d--) ++iter; } // use iterative calls to
14 else { while (d++) --iter; } // ++ or -- for other
15 } // iterator categories
16 }

 

这需要决定iter是不是一个随机访问迭代器,也就是需要知道它的类型,IterT,是不是随机访问迭代器类型。换句话说,我们需要获取一个类型的相关信息。这也是trait让你所做的:它们允许你在编译期间获取一个类型的相关信息

3. Traits技术分析

3.1 使用traits技术的要求

Traits不是C++中的关键字或者一个预定义的概念;它们是一种技术,也是一个C++程序员需要遵守的约定。使用这项技术的一个要求是它必须使内建类型同用户自定义类型一样能够很好的工作。例如,如果advance的入参为一个指针(像const char*)和一个Int,advance必须能够工作,但是这就意味着trait技术必须能够使用在像指针一样的内建类型上。

Traits必须能够同内建类型一块工作就意味着不能在类型内部嵌入一些信息,因为没有方法在指针内部嵌入信息。于是对于一种类型的traits信息,必须是放在类型外部的。标准的技术是将其放在模板和模板的一个或多个特化实例中。对于迭代器来说,标准库中的模板被命名为iterator_traits:

1 template<typename IterT> // template for information about
2 struct iterator_traits; // iterator types

 

正如你所见的,iterator_traits是一个结构体。按照惯例,traits经常被实现为一个结构体。另外一种常用手法是将实现traits的结构体替换为traits class(这不是我说的)。

Iterator_traits的工作方式是对于每个类型IterT,在结构体iterator_traits<IterT>中声明一个叫做iterator_category的typedef。这个typedef唯一确认了IterT的迭代器类别。

3.2 实现traits class需要处理用户自定义类型

Iterator_traits会在两部分中实现它。首先,它强制任何用户自定义的迭代器类型必须包含一个叫做iterator_category的内嵌typedef,它能够识别合适的tag结构体。举个例子,deque的迭代器是随机访问的,因此一个deque迭代器的类会像是下面这个样子:

 1 template < ... >                                                                         // template params elided
 2 
 3 class deque {                                                                           
 4 
 5 public:                                                                                    
 6 
 7 class iterator {                                                                        
 8 
 9 public:                                                                                    
10 
11 typedef random_access_iterator_tag iterator_category;          
12 
13 ...                                                                                             
14 
15 };                                                                                            
16 
17 ...                                                                                             
18 
19 };

 

List的迭代器是双向的,所以用下面的方式处理:

 1 template < ... >
 2 
 3 class list {
 4 
 5 public:
 6 
 7 class iterator {
 8 
 9 public:
10 
11 typedef bidirectional_iterator_tag iterator_category;
12 
13 ...
14 
15 };
16 
17 ...
18 
19 };

                                                                             

 iterator_traits只是重复使用iterator类的内嵌typedef:

 1 // the iterator_category for type IterT is whatever IterT says it is;
 2 
 3 // see Item 42 for info on the use of “typedef typename”
 4 
 5 template<typename IterT>
 6 
 7 struct iterator_traits {
 8 
 9 typedef typename IterT::iterator_category iterator_category;
10 
11 ...
12 
13 };

 

3.3 实现traits class需要处理指针类型

这对用户自定义类型来说会工作的很好,但是对于指针迭代器来说就不工作了,因为指针中没有内嵌的typedef。Iterator_trait实现的第二部分需要处理指针迭代器。

为了支持这种迭代器,iterator_traits为指针类型提供了一种部分模板特化(partial template specialization)。指针的行为表现同随机访问迭代器类似,所以iterator_trait为它们指定了随机访问类别:

1 template<typename T> // partial template specialization
2 struct iterator_traits<T*> // for built-in pointer types
3 {
4 typedef random_access_iterator_tag iterator_category;
5 ...
6 };

 

3.4 实现traits class总结

到现在你应该了解了如何设计和实现一个traits class:

  • 确认你想要支持的类型的一些信息(例如,对于迭代器来说,它们的迭代器类别)。
  • 为了确认信息,你需要选择一个名字(例如,iterator_category)
  • 为你想支持的类型提供包含相关信息的一个模板和一些特化(例如,iterator_traits)

4. 使用traits class实现advance

4.1 类别判断不应该在运行时进行

考虑iterator_traits——实际上是std::iterator_traits,既然它是C++标准库的一部分——我们能为advance的实现精炼成我们自己的伪代码:

1 template<typename IterT, typename DistT>
2 void advance(IterT& iter, DistT d)
3 {
4 if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
5 typeid(std::random_access_iterator_tag))
6 ...
7 }

 

虽然这看上去很有希望,但它不会如我们所愿。第一,它会导致编译问题,这个问题我们将在Item 48研究;现在,有更加基本的问题需要考虑。IterT的类型在编译期被确认,所以iterator_traits<IterT>::iterator_category同样能够在编译期被确定。但是if语句会在运行时被评估(除非你的优化器足够疯狂,把if语句去掉)。为什么将本来可以在编译期做的事情挪到运行时去做呢?它会浪费时间,并且会造成执行代码膨胀。

4.2 将条件评估提前到编译期——使用重载

我们真正想要的是为类型提供一个能够在编译期进行评估的条件结构(也就是一个if…else语句)。C++已经有一种方法来实现这种行为。她叫做重载

当你重载某个函数f的时候,你为不同的重载函数指定不同的参数类型。当你调用f时,编译器会根据你所传递的参数选择最佳匹配重载函数。编译器会说:“如果这个重载对于传递过来的参数来说是最佳匹配,那么调用这个f;如果另外一个重载函数是最佳匹配,那么调用另外一个函数;如果第三个函数是最佳匹配,调用第三个”等等,看到了么?这是一个与类型相关的编译期条件结构。为了让advance表现出我们想要的行为,所有我们必须要做的是创建一个重载函数的多个版本,它们包含了advance的“内脏”,每个函数都带有一个不同类型的iterator_category对象。我将这些函数命名为doAdvance:

 1 template<typename IterT, typename DistT> // use this impl for
 2 void doAdvance(IterT& iter, DistT d, // random access
 3 std::random_access_iterator_tag) // iterators
 4 {
 5 iter += d;
 6 }
 7 template<typename IterT, typename DistT> // use this impl for
 8 void doAdvance(IterT& iter, DistT d, // bidirectional
 9 std::bidirectional_iterator_tag) // iterators
10 {
11 if (d >= 0) { while (d--) ++iter; }
12 else { while (d++) --iter; }
13 }
14 template<typename IterT, typename DistT> // use this impl for
15 void doAdvance(IterT& iter, DistT d, // input iterators
16 std::input_iterator_tag)
17 {
18 if (d < 0 ) {
19 throw std::out_of_range("Negative distance"); // see below
20 }
21 while (d--) ++iter;
22 }

 

因为forward_iterator_tag继承自input_iterator_tag,为input_iterator_tag提供的doAdvance版本同样能够处理forward迭代器。这是在不同iterator_tag结构体之间引入继承的动机。(事实上,这也是所有的使用public继承的动机:为基类类型实现的代码对于派生类类型来说同样适用。)

对于随机访问迭代器和双向迭代器来说,advance的特化版本同时能够做正向或者负向的移动,但是对于forward迭代器或者input迭代器来说,如果你想进行一个负向的移动就会出现未定义行为。实现中如果简单的假设d是非负的,当传递一个负参数时,你就会进入一个很长的循环中,直到d变为0为止。在上面的代码中,我所使用的替代方法是抛出一个异常。两种实现都是有效的。这就是未定义行为的诅咒:你不能预测出来会发成什么。

考虑为doAdvance所重载的不同版本,所有advance需要做的就是调用它们,传递一个额外的合适的迭代器类别对象,最后编译器就能够使用重载方案来调用合适的实现:

 1 template<typename IterT, typename DistT>
 2 void advance(IterT& iter, DistT d)
 3 {
 4 doAdvance( // call the version
 5 iter, d, // of doAdvance
 6 typename // that is
 7 std::iterator_traits<IterT>::iterator_category() // appropriate for
 8 ); // iter’s iterator
 9 
10 }                                                                                  // category

 

5. traits class使用总结

 

我们可以总结一下如何使用traits class:

  • 创建一系列重载的”worker”函数或者函数模板(例如,doAdvance),通过使用traits 参数来进行区分。根据传递的traits信息来对应的实现每个函数。
  • 创建一个“master”函数或者函数模板(例如,advance)来调用worker,将traits class提供的信息传递进去。

 

Traits被广泛使用在标准库中。对于iterator_traits来说,除了iterator_category,还为迭代器提供了四种其它的信息(最有用的就是value_type—Item 42中给出了一个例子。)还有char_traits,存储了字符类型的信息,numeric_limits,提供数字类型信息,例如,它们能够表示的最大和最小值等等。(numeric_limits这个名字可能让你感到意外,因为传统的命名方式是以“traits”结尾,但是numeric_limits没有遵守。)

 

TR1(Item 54)为了为类型提供信息引入了大量的新的traits class,包括is_fundamental<T>(判断T是否为内建类型),is_array<T>(判断T是否为数组),和is_base_of<T1,T2>(判断T1和T2是否相同或者是T2的基类)。TR1向标准C++中添加了大概有50个traits classes。

 

6. 本条款总结 

  • Traits classes使得在编译期就能够获得类型信息。它们用模板和模板特化版本来进行实现。
  • 利用重载,traits classes使得在类型上执行编译期if-else测试成为可能。
posted @ 2017-04-13 08:40  HarlanC  阅读(1157)  评论(0编辑  收藏  举报