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测试成为可能。

作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

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

  1. 读书笔记 effective c++ Item 50 了解何时替换new和delete 是有意义的

    1. 自定义new和delete的三个常见原因 我们先回顾一下基本原理.为什么人们一开始就想去替换编译器提供的operator new和operator delete版本?有三个最常见的原因: 为了检 ...

  2. 读书笔记 effective c++ Item 49 理解new-handler的行为

    1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨 ...

  3. 读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用

    1. 什么样的接口才是好的接口 C++中充斥着接口:函数接口,类接口,模板接口.每个接口都是客户同你的代码进行交互的一种方法.假设你正在面对的是一些"讲道理"的人员,这些客户尝试把 ...

  4. 读书笔记 effective c++ Item 16 成对使用new和delete时要用相同的形式

    1. 一个错误释放内存的例子 下面的场景会有什么错? 1 std::string *stringArray = new std::string[100]; 2 3 ... 4 5 delete str ...

  5. 读书笔记 effective c++ Item 41 理解隐式接口和编译期多态

    1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), 1 class Widget { 2 public: 3 Widget(); 4 vi ...

  6. 读书笔记 effective c++ Item 34 区分接口继承和实现继承

    看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...

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

    正文 最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热 ...

  8. 读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数

    1 编译器会默认生成哪些函数  什么时候空类不再是一个空类?答案是用c++处理的空类.如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声 ...

  9. [读书笔记]Effective C++ - Scott Meyers

    [读书笔记]Effective C++ - Scott Meyers 条款01:视C++为一个语言联邦 C++四个次语言: 1. C Part-of-C++,没有模板.异常.重载. 2. Object ...

最新文章

  1. js 将时间戳转为日期格式
  2. Kubernetes初体验
  3. linux7主机名设置,centos7主机名、网络设置
  4. python装饰器编程_Python编程中装饰器的使用示例解析
  5. Hive 导数据到本地(2种方式)
  6. pad_sequences序列预处理
  7. 已解决 selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element 找不到元素的问题
  8. 1087 1 10 100 1000
  9. SQLSERTVER安装教程
  10. 【Java开发bug-011】excel设置自定义日期格式
  11. 做对的事情远比把事情做对重要得多
  12. 最美春三月,又到一年迎新时——烟台北大青鸟喜迎2019级新生入学报到
  13. python定义一个student类、有下面的_Python的类和方法——成员可见性
  14. JS 数组 isAarray() typeof push() unshift() splice()替换/删除/插入 slice()切片 join() split() reverse concat
  15. Caffe中BN层与CONV层的融合(merge_bn)
  16. 标签分发协议(LDP)
  17. fortran常见错误汇总
  18. 使用Vmalert监控报警
  19. 冒泡算法详解(AVA)
  20. 【AIoT库】RFID基础知识第5期 · 市场应用及前景

热门文章

  1. oracle 查看最大连接数与当前连接数
  2. python-day74--知识总体总结
  3. 少年Vince之遐想
  4. 炼丹手册——学习率设置
  5. stl变易算法(三)
  6. python使用函数输出指定范围内fibonacci数的个数_第6章函数-4 使用函数输出指定范围内Fibonacci数的个数...
  7. 软件测试 java_关于Java单元测试,你需要知道的一切
  8. python百度语音实时识别成文字_python 上传百度语音识别+文字返回结果
  9. mysql 中文字符 函数_MySQL基础之字符函数-Go语言中文社区
  10. 高德h5地图api接口_在uniapp h5中在线引入高德地图js api的方法分享