effective C++ 条款 47:使用traits classes表现类型信息
stl主要由“用以表现容器、迭代器和算法”的template构成,但也覆盖若干工具性的templates,其中一个名为advance,将某个迭代器移动某个给定距离:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); //将迭代器向前移动d个单位,d<0则向后移动。
stl迭代器的分类:
Input迭代器只能向前移动,一次一步,只读,且只能读取一次。模仿指向输入文件的阅读指针:istream_iterator是代表
Output迭代器类似,但只可涂写它们所指的东西,ostream_iterator是代表。
它们只适合“一次性操作算法”one-pass algorithm。
forward迭代器,可以做前述两种分类所能做的每一件事,而且可以读或写其所指物一次以上。可施行于多次性操作算法。stl未提供单向linked_list,指入这种容器的迭代器就是属于forward迭代器。
bedirectional迭代器比上一个威力更大:除了可以向前移动,还可以向后移动。stl的list、set、multiset、map和multimap的迭代器都属于这一类。
random access迭代器比上一个更威力大,它可以执行迭代器算术,常量时间内可以向前或向后跳跃任意距离。内置指针也可以当random迭代器用。vector、deque、string提供的迭代器都是这一类。
这5种分类,c++标准程序库分别提供专属的卷标结构(tag struct)加以确认:
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag: public input_iterator_tag{};
struct bidirectional_iterator_tag: public forward_iterator_tag{};
struct random_iterator_tag: public bidirectional_iterator_tag{};
我们真正希望的是以这种方式实现advance:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (iter is a random_access_iterator)
{
iter += d; //针对random_access迭代器使用迭代器算术运算
}
else
{
if (d >= 0)
{
while (d--)
{
++iter;
}
}
else
{
while (d++)
{
--iter;
}
}
}
}这种做法必须先判断iter是否为random_access迭代器,需要取得类型的某些信息。那就是traits让你进行的事:它们允许在编译期间取得某些类型信息。
traits并不是c++关键字或一个预先定义好的构件:它们是一种技术,也是一个c++程序员共同遵守的协议。这个技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好。
“traits必须能够施行于内置类型”意味着“类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原始指针内。因此类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其一或多个特化版本中。这样的templates在标准库中有若干个,其中针对迭代器的被命名为iterator_traits:
template<typename IterT>//template,用来处理迭代器分类的相关信息
struct iterator_traits;习惯上traits总是被实现为structs,但它们却又往往被称为traits classes。
针对每个IterT,在struct iterator_traits<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。
两个部分实现上述所言。首先它要求每一个“用户自定义的迭代器类型”必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构(tag struct)。例如:deque和list的迭代器的样子
template<...>
class deque{
public:
class iterator{
public:
typedef random_access_iterator_tag iterator_category;
};
};template<...>
class list{
public:
class iterator{
public:
typedef bidirectional_iterator_tag iterator_category;
};
};至于iterator_traits,只是鹦鹉学舌般地响应iterator class的嵌套式typedef:
template<typename IterT>
struct iterator_traits{
typedef typename IterT::iterator_category iterator_category;
...
};iterator_traits的第二部分,专门用来对付指针,因为指针不含typedef。为了支持指针迭代器,iterator_traits特别针对指针类型提供了一个偏特化版本(partial template specialization)。指针的行径与random_access迭代器相似,多以iterator_traits为指针指定的迭代器类型是:
template<typename IterT> //template 偏特化,针对内置指针
struct iterator_traits<IterT*>{
typedef random_access_iterator_tag iterator_category;
};
实现一个traits class:
确认托干你希望将来可取得的类型相关信息。例如迭代器而言,我们希望将来可以取得其分类(category)。
为该信息选择一个名称(例如iterator_category)
提供一个template和一组特化版本,内含你希望支持的类型相关信息。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (typeid(typename std::iterator_traits<IterT>::iterator_category)
== typeid(std::random_access_iterator_tag))
{
...
}
}并非我们想要的,首先会导致编译问题(条款48)。IterT类型在编译期间获得,所以iterator_traits<IterT>::iterator_category也可以在编译期间确定。但if语句却是在运行期才会核定。这不仅浪费时间,也造成可执行文件膨胀。
我们真正想要的是一个条件式判断“编译期核定成功”之类型。c++有一个取得这种行为的办法,那就是重载(overloading)
你必须详细叙述各个重载件的参数类型。当你调用f,编译器便根据传来的实参选择最适当的重载件。
template<typename IterT, typename DistT> //这份实现于
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) //random access
{ //迭代器
iter += d;
}
template<typename IterT, typename DistT> //这份实现于
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) //biderectional
{ //迭代器
if (d >= 0) {while (d--) ++iter;}
else {while(d++) --iter;}
}
template<typename IterT, typename DistT> //这份实现于
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) //input
{ //迭代器
if (d < 0){throw std::out_of_range("Negative distance");}
while (d--) ++iter;
}由于forward_iterator_tag继承自input_iterator_tag,所以上述doAdvance的input_irerator_tag版本也能够处理forward迭代器。
advance需要做的只是调用它们并额外传递一个对象,后者必须带有适当的迭代器分类。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}使用traits class:
建立一组重载函数(身份像劳工)或函数模板,(例如doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受的traits信息相应和。
建立一个控制函数(身份像工头)或函数模板,例如advance,它调用那些“劳工函数”并传递traits class所提供的信息。