47:请使用traits classes(特质类)表现类型信息

STL共有5种迭代器分类,对应于它们支持的操作。

Input迭代器只能向前移动,一次一步,客户只可读取(不能涂写)它们所指的东西,而且只能读取一次。它们模仿指向输入文件的阅读指针。C++程序库种的istream_iterator是这一分类的代表。

Output迭代器情况类似,但一切只为输出。它们只向前移动,一次一步,客户只可涂写它们所指的东西,而且只能涂写一次。它们模仿指向输出文件的涂写指针。ostream_iterator是这一类的代表。

这是威力最小的两个迭代器分类 。由于这两类都只能向前移动,而且只能读或写其所指物最多一次,所以它们只适合“一次性操作算法”。

另一个威力比较强大的分类是forward迭代器。这种迭代器可以做前述两种分类所能做的每一件事,而且可以读或写其所指物一次以上。这使得它们可施行于多次行操作算法。STL并未提供单向linked list,但某些程序库有(通常名为slist),而指入这种容器的迭代器就属于forward迭代器。

Bidirectional迭代器比上一个分类威力更大,它除了可以向前移动,还可以向后移动。STL的list迭代器就属于这一分类,set,multiset,map和multimap的迭代器也都是这一分类。

最有威力的当属random access迭代器。这种迭代器比上一个分类威力更大的地方在于它可以执行“迭代器算术”,也就是它可以在常量时间内向前或向后跳跃任意距离。这样的算术类似指针算术。

对于这5种分类,C++标准程序库分别提供专属的卷标结构加以确认:

struct input_itreator_tag{};
struct output_itreator_tag {};
struct forward_itreator_tag :public input_itreator_tag {};
struct bidirectional_itreator_tag :public forward_itreator_tag {};
struct random_itreator_tag :public bidirectional_itreator_tag {};

STL主要由“用以表现容器、迭代器和算法”的template构成,但也覆盖若干工具性template,其中一个名为advance,用来将某个迭代器移动某个给定距离:

//将迭代器向前移动d单位,若d<0则向后移动
template<typename IterT,typename DistT>
void advance(IterT& iter, DistT d);

观念上advance只是做iter+=d动作,但其实不可以全然那么实践,因为只有random acess(随机访问)迭代器才支持+=操作。而对其他威力不那么强大的迭代器种类,advance必须随机反复施行++或--,共d次。

实现advance的策略之一是采用“最低但最普及”的迭代器能力,以循环反复递增或递减迭代器。然而这种做法耗费线性时间。random access迭代器支持迭代器算术运算,只耗费线性时间,因此若面对这种迭代器,我们希望运用其优势。

我们真正希望的是以这种方式实现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迭代器,也就是说需要知道类型IterT是否为random access迭代器分类。换句话说,我们需要取得类型的某些信息。那就是trait让你得以进行的事:它们允许你在编译期间取得某些类型信息。

trait并不是C++关键字或一个预先定义好的构件,它们是一种技术,也是一个C++程序员共同遵守的协议。这个技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好。

例如,若上述advance收到的实参是一个指针(例如const char*)和一个int,上述advance仍然必须有效运作,这意味trait技术必须也能够施行于内置类型如指针身上。

“trait必须能够施行于内置类型”意味“类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原始指针内。因此类型的trait信息必须位于类型自身之外。标准技术是将它放进一个template及其一或多个特化版本中。这样的template在标准程序库中有若干个,其中针对迭代器被命名为iterator_traits:

//template,用来处理迭代器分类的相关信息
template<typename IterT>
struct iterator_traits;

iterator_traits的运作方式是:针对每一个类型IterT,在struct iterator_traits<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。

iterator_traits以两个部分实现上述所言。首先,它要求每一个“用户自定义的迭代器类型”必须嵌套一个typedef,名为iterator_caegory,用来确认适当的卷标结构。例如deque的迭代器可随机访问,所以一个针对deque迭代器而设计的class看起来是这样:

template</*...*/>//略而未写template参数
class deque {
public:
    class iterator {
    public:
        typedef random_access_iterator_tag iterator_category;
        //...
    };
    //...
};

list的迭代器可双向行进,所以它应该是这样:

template</*...*/>//略而未写template参数
class list {
public:
    class iterator {
    public:
        typedef bidirectional_iterator_tag iterator_category;
        //...
    };
    //...
};

至于iterator_traits,只是鹦鹉学舌般地响应iterator class的嵌套式typedef:

//类型IterT的iterator_category其实就是用来表现“IterT说它自己是什么”
template<typename IterT>
struct iterator_traits {
    typedef typename IterT::iterator_category iterator_category;
    //...
};

这对用户自定义类型行得通,但对指针(也是一种迭代器)行不通,因为指针不可能嵌套typedef。

iterator_traits的第二部分如下,专门用来对付指针。

为了支持指针迭代器,iterator_traits特别针对指针类型提供一个偏特化版本。由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:

//template偏特化,针对内置指针
template<typename IterT>
struct iterator_traits<IterT*> {
    typedef typename IterT::iterator_category iterator_category;
    //...
};

现在,你应该知道了如何设计并实现一个traits class了:

1.确认若干你希望将来可取得的类型相关信息。

例如对迭代器而言,我们希望将来可取得其分类。

2.为该信息选择一个名称(例如iterator_category)。

3.提供一个template和一组特化版本,内含你希望支持的类型相关信息。

现在,可以对advance实践先前的伪码:

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))
    //...
}

但上述代码并非我们想要的。首先它会导致编译问题。

此刻有更根本的问题要考虑。IterT类型在编译期间获知,所以iterator_traits<IterT>::iterator_category也可在编译期间确定。但if语句却是在运行期才会核定。为什么可将编译期完成的事延到运行期才做呢?这不仅浪费时间,也造成可执行文件膨胀。

我们真正想要的是一个条件式(也就是一个if...else语句)判断“编译期核定成功”的类型。恰巧C++有一个取得这种行为的办法,就是重载。

为了让advance的行为如我们所期望,我们需要做的是产生两版重载函数,内含advance的本质内容,但各自接受不同类型iterator_category对象。将这两个函数取名为doAdvance:

//这份实现用于random access迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
    iter += d;
}
//这份实现用于bidirectional迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag)
{
    if (d >= 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
}
//这份实现用于input迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
{
    if (d < 0) {
        throw std:out_of_range("Negative distance");
    }
    while (d--) ++iter;
}

由于forward_iterator_tag继承自input_iterator_tag,所以上述doAdvance的input_iterator_tag版本也能够办理forward迭代器。

有了这些doAdvance重载版本,advance需要做的只是调用它们并额外传递一个对象,后者必须带有适当的迭代器分类。于是编译期运用重载解析机制调用适当的实现代码:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
    //调用的doAdvance版本,对iter的迭代器分类而言必须是适当的
    doAdvance
    {
        iter, d,
        typename
        std::iterator_traits<IterT>::iterator_category()
    };
}

现在可以总结如何使用一个traits class:

1.建立一组重载函数(身份像劳工)或函数模板(如doAdvance),彼此间的差异只在于各自的trait参数。令每个函数实现码与其接受的trait信息相应和。

2.建立一个控制函数(身份像工头)或函数模板(如advance),它调用上述那些“劳工函数”并传递traits class所提供的信息。

traits广泛应用于标准程序库。其中当然有上述讨论的iterator_traits,除了供应iterator_category还供应另四份迭代器相关信息(其中最有用的是value_type)。此外还有char_traits用来保存字符类型的相关信息,以及numeric_limits用来保存数值类型的相关信息,例如某数值类型可表现的最小值和最大值等等。

TR!导入许多新的trait class用以提供类型信息,包括is_fundamental<T>(判断T是否为内置类型),is_array<T>(判断T是否为数组类型),以及is_base_of<T1,T2>(T1和T2相同,抑或T1是T2的base class)。总计TR1一共为标准C++添加了50个以上的trait class。

总结

1.traits class使得“类型相关信息”在编译期可用。它们以template和“template特化”完成实现。

2.整合重载技术后,traits class有可能在编译期对类型执行if..else测试。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值