读书笔记 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测试成为可能。
时间: 2024-10-18 01:03:50

读书笔记 effective c++ Item 47 使用traits class表示类型信息的相关文章

读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C++标准预计在2009年被发布(虽然所有的工作很有可能在2007年底被完成).直到现在,发布下一版C++的预计年份还没有被确定,这就解释了为什么人们把下一版C++叫做“C++0x”——C++的200x年版本. C++0x可能会包含一些有趣的新的语言特性,但是大多数新C++功能将会以标准库附加物的形式被

读书笔记 effective c++ Item 55 让你自己熟悉Boost

你正在寻找一个高质量的,开源的,与平台和编译器无关的程序库的集合?看一下Boost吧.想加入一个由雄心勃勃的,充满天赋的正致力于最高水平的程序库设计和实现工作的C++程序员们组成的团体么?看一下Boost吧.想了解C++将来可能会是什么样子的?看一下Boost吧. Boost是一个C++开发人员组成的团体,也是供免费下载的C++程序库的集合.网址是http://boost.org. 1. Boost的两大优势 当然,有许多C++组织和网站,但是Boost有两点是其它组织不能与之媲美的.首先,它和

读书笔记 effective c++ Item 48 了解模板元编程

1. TMP是什么? 模板元编程(template metaprogramming TMP)是实现基于模板的C++程序的过程,它能够在编译期执行.你可以想一想:一个模板元程序是用C++实现的并且可以在C++编译器内部运行的一个程序,它的输出——从模板中实例化出来的C++源码片段——会像往常一样被编译. 2. 使用TMP的优势 如果这没有冲击到你,是因为你没有足够尽力去想. C++不是为了模板元编程而设计的,但是自从TMP早在1990年被发现之后,它就被证明是非常有用的,为了使TMP的使用更加容易

读书笔记 effective c++ Item 35 考虑虚函数的替代者

1. 突破思维——不要将思维限定在面向对象方法上 你正在制作一个视频游戏,你正在为游戏中的人物设计一个类继承体系.你的游戏处在农耕时代,人类很容易受伤或者说健康度降低.因此你决定为其提供一个成员函数,healthValue,返回一个整型值来表明一个人物的健康度.因为不同的人物会用不同的方式来计算健康度,将healthValue声明为虚函数看上去是一个比较明显的设计方式: 1 class GameCharacter { 2 public: 3 4 virtual int healthValue()

读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来

1. 使用模板可能导致代码膨胀 使用模板是节省时间和避免代码重用的很好的方法.你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定的类和300个你需要的函数.(只有在被使用的情况下类模版的成员函数才会被隐式的实例化,所以只有在300个函数被实际用到的情况下才会生成300个成员函数.)函数模板同样吸引人.你不用手动实现许多函数,你只需要实现一个函数模板,然后让编译器来做余下的事情. 然而在有些时候,如果你不小心,使用模板会导致代

读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型

1. 何为public继承的”is-a”关系 在C++面向对象准则中最重要的准则是:public继承意味着“is-a”.记住这个准则. 如果你实现一个类D(derived)public继承自类B(base),你在告诉c++编译器(也在告诉代码阅读者),每个类型D的对象也是一个类型B的对象,反过来说是不对的.你正在诉说B比D表示了一个更为一般的概念,而D比B表现了一个更为特殊的概念.你在主张:任何可以使用类型B的地方,也能使用类型D,因为每个类型D的对象都是类型B的对象:反过来却不对,也就是可以使

读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)

最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热情和追求还是不减,应该是感动了周公吧,梦境从此处开始,大师入场来给我安慰了... 11点躺在床上了,脑子里总结一下最近的工作:最近的开发用到inline函数比较多,众所周知,inline的使用是为了提高程序性能,可结果却总不尽如人意,这个捉急啊,嗯?怎么突然到了山脚下,周边树木林立,郁郁葱葱,鸟儿委婉啼叫

读书笔记 effective c++ Item 11 在operator=中处理自我赋值

1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: 1 class Widget { ... }; 2 3 Widget w; 4 5 ... 6 7 w = w; // assignment to self. 这看上去是愚蠢的,但这是合法的,所以请放心,客户端是可以这么做的.此外,自身赋值也并不总是很容易的能够被辨别出来.举个例子: 1 a[i] = a[j]; // potential assignment to self 上面的代码在i和j相等的情况下就是自我赋值,同

读书笔记 effective c++ Item 53 关注编译器发出的警告

许多程序员常常忽略编译器发出的警告.毕竟,如果问题很严重,它才将会变成一个error,不是么?相对来说,这个想法可能在其它语言是无害的,但是在C++中,我敢打赌编译器的实现者对于对接下来会发生什么比你有更好的理解.例如,下面的错误是每个人都时不时会犯的: 1 class B { 2 public: 3 virtual void f() const; 4 }; 5 class D: public B { 6 public: 7 virtual void f(); 8 }; 1. 你对警告的理解可能