一. 内容
-
本条款仍然以例子入手,假如你要设计
一个 Move 函数,用于将 STL 的迭代器移动指定的距离
:template <typename IteratorType> void Move(IteratorType & Iterator,int Distance);`在这里插入代码片`
-
我们先来认识一下 STL 迭代器的分类,迭代器共有 5 种类型:
- Input 迭代器,只能一步一步向前移动,只能读取内容,且仅读取一次。
- Output 迭代器,只能一步一步向前移动,只能写入内容,且仅写入一次。
- Forward 迭代器,只能一步一步向前移动,可读可写多次。
- Bidirectianol 迭代器,只能一步一步向前或向后移动,可读可写多次。
Random Access 迭代器
,可以在常量时间以任意步数向前或向后移动
,可读可写多次。
可以看到,迭代器的功能越来越多,有些功能是前面迭代器不具备的,这意味着当我们编写一个模板函数时,
一句 Iterator+=Distance 已经无法满足我们的要求了
,有些迭代器是不支持大于1的距离跳跃的,只能反复的进行 ++ 或 --。所以这需要我们根据迭代器的类型判断进行何种操作。 -
通过对迭代器的认识,我们知道只有 random access iterator 具有跳跃能力,那么伪代码可以写成这样:
template <typename IteratorType> void Move(IteratorType & Iterator,int Distance) { if ("Iterator is a random access iterator") { Iterator+=Distance; }else { if (Distance>=0) { while(Distance--)++Iterator; }else { while(Distance++)--Iterator; } } }
关键是我们怎么在编译期获得这个参数的类型信息呢?
-
Traits 技术解决了这个问题,但它并不是一个 C++ 关键字或者某个预先定义的内置部件。这项技术的要求之一是:
它对内置(built-in)类型和用户自定义(user-defined)类型表现必须一样好
。举个例子,假如上述 Move 函数收到一个指针和一个整数,代码仍然必须有效运作。Traits 必须能够施行于内置类型,意味着类型内的嵌套信息出局了。因为
原始指针并不具备嵌套信息的能力
。因此类型的 traits 信息必须位于类型自身之外。标准做法是:
将 traits 信息放进 template 和其多个特化版本中
。一个典型的实现是:struct InputIteratorTag {}; // 枚举类型信息 struct OutputIteratorTag {}; struct ForwardIteratorTag : public InputIteratorTag {}; struct BidirectionalIteratorTag : public ForwardIteratorTag {}; struct RandomAccessIteratorTag : public BidirectionalIteratorTag {}; template <typename T> class RandomAccessIterator { public: typedef RandomAccessIteratorTag IteratorTag; // 声明类型信息 RandomAccessIterator& operator++() { //... return *this; } RandomAccessIterator& operator--() { //... return *this; } RandomAccessIterator& operator+=(int) { //... return *this; } }; template <typename T> struct IteratorTraits { typedef typename T::IteratorTag IteratorTag; // 模板 Traits,放置在类型之外 }; template <typename T> struct IteratorTraits<T*> { //针对内置指针的特化 Traits typedef RandomAccessIteratorTag IteratorTag; }; template <typename IteratorType> void Move(IteratorType& Iterator, int Distance) { // 使用类型信息 if (typeid(IteratorTraits<IteratorType>::IteratorTag) == typeid(RandomAccessIteratorTag)) { Iterator += Distance; } else { if (Distance >= 0) { while (Distance--)++Iterator; } else { while (Distance++)--Iterator; } } } inline void TryWithTraits() { RandomAccessIterator<int> Iterator; Move(Iterator, 100); int *Other=new int[100]; Move(Other, 100); }
-
现在你应该知道如何设计并实现一个 traits class了:
- 确认若干你希望将来可取得的类型相关信息。例如迭代器,我们希望获得其分类 IteratorTag。
- 为该信息选择一个名称。例如 typedef xxx IteratorTag。
- 提供一个 template 和一组特化版本,内含你希望支持的类型相关信息。例如 typedef typename T::IteratorTag IteratorTag。
-
虽然这一切看起来良好,但是存在潜在的编译问题,这点在条款48会讲述,但有一个更重要的问题:类型相关的信息我们可以在编译器确定,为什么我们要拖到运行期才完成 if 语句的核定呢?这不仅浪费时间,还造成可执行文件的膨胀。
我们真正想要的是
一个可以在编译期进行 if else 式编程的方法
,恰巧的是,C++有这种方法:重载
。当你重载某个函数 f,你必须详尽各个重载函数的参数类型。当你调用 f,编译器便根据传来的实参选择最适当的重载函数。编译器的态度是如果这个重载函数合适,就选用这个 f,如果另一个重载函数更合适,就会选用另一个。看到没,这就是编译期可以发生的 if else 式编程的方法。这意味我们可以采用重载的方法重新设计 traits :template <typename IteratorType> void OnMove(IteratorType& Iterator, int Distance,InputIteratorTag) { if (Distance<0) { throw std::out_of_range(""); } while(Distance--)++Iterator; } //... template <typename IteratorType> void OnMove(IteratorType& Iterator, int Distance,BidirectionalIteratorTag) { if (Distance >= 0) { while (Distance--)++Iterator; } else { while (Distance++)--Iterator; } } template <typename IteratorType> void OnMove(IteratorType& Iterator, int Distance,RandomAccessIteratorTag) { Iterator+=Distance; } template <typename IteratorType> void Move(IteratorType& Iterator, int Distance) { OnMove(Iterator,Distance,typename IteratorTraits<IteratorType>::IteratorTag()); }
可以看出,通过 OnMove 函数的重载,我们在编译期实现了 Traits 技术。
-
现在我们可以进一步总结如何使用 traits class了:
- 建立一组重载函数或函数模板,例如 OnMove,用于对不同的 traits 参数的进行区别实现。
- 建立一个控制函数或函数模板,例如 Move,调用上述重载函数并传递 traits class 提供的类型信息。
-
虽然我们说 traits class,习惯上 traits 总是被实现为 structs,可能考虑到只是简单的传递信息,
访问权限默认 public 比较方便
。
二. 总结
- Traits classes 使得类型相关信息在编译器可用。它们以 templates 和 templates 特化完成实现。
- 整合重载技术(overloading)后,Traits classes 有可能在编译器对类型执行 if…else 测试。