《Effective C++》学习笔记(条款47:请使用traits class表现类型信息)

最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!

STL主要由“用以表现容器、迭代器和算法”的 模板 构成,也包括若干工具性质的 模板 ,其中有一个 advance 用来将迭代器移动某个给定距离:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);			//d大于零,迭代器向前移动d单位,小于零则向后移动

一般来说,只是 iterate+=d 的动作,但并不是所有迭代器都是这么实现的,只有支持随机访问的迭代器才支持 += 操作。其他类型没这么大威力的迭代器,只有反复 ++--,共 d 次。

迭代器的类型有5种(C++17前,C++17起有6种,剩下的一种是 ContiguousIterator(连续迭代器))。

迭代器类别说明
输入迭代器只能向前移动,一次一步,只可从容器中读取(不能写入)元素,且只能读取一次。该分类下只有 istream_iterator和istreambuf_iterator
输出迭代器只能向前移动,一次一步,只可向容器写入(不能读取)元素,且只能写入一次。该分类下有 ostream_iterator和ostreambuf_iterator
前向迭代器组合输入迭代器和输出迭代器的功能,而且可以读写元素一次以上
双向迭代器组合前向迭代器和逆向迭代器的功能,该分类下有 list 、set、multiset、map和multimap的迭代器
随机访问迭代器组合双向迭代器的功能与可以执行“迭代器算术运算”,即它可以在常量时间内向前或向后跳过任意个元素。该分类下有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_access_iterator_tag : public bidirectional_iterator_tag {};

这些结构之间的继承关系是 is-a 关系(见条款32):所有的前向迭代器都是输入迭代器。

我们知道 STL 迭代器有着不同的能力,实现 advance 的策略之一是采用“最低但最普及”的迭代器能力,以循环反复递增或递减迭代器。然而这种做法耗费线性时间。我们知道随机访问迭代器支持迭代器算术运算,只耗费常量时间,所以我们希望以下面的方式实现 advance:

template<typename IterT, typename DistT>
void advance(IterI &iter, DistT d)
{
    if (iter is a random access iterator)
    {
        iter += d;				//针对随机访问迭代器使用迭代器算术运算
    } else {
        if (d >= 0) {			//针对其它迭代器分类反复调用 ++ 或 --
            while(d--) ++iter;
        } else {
            while (d++) --iter;
        }
    }
}

在上面实现中要判断 iter 是否为 随机访问迭代器,也就是说需要知道 IterT 类型是否为 random access 类型。这就需要 traits,它允许我们在编译期间获取某些类型信息。

Traits 并不是C++关键字或一个预先定义好的构件;它们是一种技术,也是一个C++程序员共同遵守的协议。这个技术要求之一是,它对内置类型 和 自定义类型 的表现必须一样好。举个例子,如果 advance 收到的实参是一个指针(如const char*)和一个 int,上述 advance 仍然必须有效运作,那意味 traits 技术必须也能够施行于内置类型,如指针身上。

traits必须能够施行于内置类型,意味着“类型内的嵌套信息”这种东西没用了,因为我们无法将信息嵌套于原值指针内。因此类型的 traits信息必须位于类型自身之外。标准技术是把它放进一个 模板 及其一个或多个特化版本中。这样的 模板 在STL中有若干个,其中迭代器的为 iterator_traits:

template<typename IterT>//用来处理迭代器分类
struct iterator_traits;

iterator_traits 是一个 struct 。习惯上 traits 总是被实现为 struct,但它们往往又被称为 traits classes。

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

综上所述,iterator_traits 首先要求每一个“用户自定义的迭代器类型”必须嵌套一个 typedef,名为 iterator_category,用来确认适当的 卷标结构:

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

template< ... >
class list{			//list的迭代器
public:
    class iterator{
    public:
        typedef bidirectional_iterator_tag iterator_category;
        ...
    };
    ...
};

template<typename IterT>//IterT的iterator_category就是用来表现IterT说自己是什么
struct iterator_traits{
    //typedef typename的使用,见条款42
    typedef typename IterT::iterator_category iterator_category;
    ...
};

这样对用户自定义类型行得通,但是对指针(也是一种迭代器)行不通,因为指针不可能嵌套 typedef。下面就是iterator_traits 的第2部分了,专门用来支持指针。

为了支持指针迭代器,iterator_traits 特别针对类型提供一个偏特化版本:

template<typename IterT>
struct iterator_traits<IterT*>//针对内置指针
{
    typedef random_access_iterator_tag iterator_category;
    ...
};

现在可以知道如何设计并实现一个traits class了:

  • 确认若干我们希望将来可取得的类型相关信息。例如对迭代器来说,就是可以取得其分类
  • 为该信息选择一个名称(例如 iterator_category
  • 提供一个 template 和一组特化版本,内含你希望支持的类型和相关信息

现在有了 iterator_traits ,可以实现一下advance了,对于之前advance 中的伪代码,替换为:

template<typename IterT, typename DistT>
void advance(IterT& iter,DisT d)
{
    if(typeid(typename std::iterator_traits<IterT>::iterator_category) ==
       typeid(std::random_access_iterator_tag))
    ...
}

虽然逻辑是正确,但并非是我们想要的,抛开编译问题不说(见条款48),还有一个更根本的问题:IterT 类型在编译期得知,所以 iterator_traits::iterator_category 也在编译期确定。但是 if 语句却是在运行期核定。可以在编译期完成的事情却推到运行期,这不仅浪费时间,还造成执行文件膨胀。

要在编译期确定,可以使用重载。重载是在编译期确定的,编译器会找到最匹配的函数来调用。为了让 advance 的行为如我们所期望,我们需要做得是产生两版重载函数,内含 advance 的本质内容,但各自接受不同类型的 iterator_category 对象,这个函数取名为 doAdvance:

template<typename IterT, typename DisT>
void doAdvance(IterT& iter, Dist d, std::random_access_iterator_tag) {	//随机访问迭代器
    iter += d;
}

template<typename IterT, typename DisT>
void doAdvance(IterT& iter, Dist d, std::bidirectional_iterator_tag) {	//双向迭代器
    if(d >= 0)
        while(d--) ++iter;
    else 
        while(d++) --iter;
}

template<typename IterT, typename DisT>
void doAdvance(IterT& iter, Dist d, std::input_iterator_tag) {			//输入迭代器
    if(d < 0)
        throw std::out_of_range("Negative distance");
    while(d++) --iter;
}
//在advance中调用doAdvance
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的信息

Traits广泛应用标准程序库,除了上面所说的iterator_traits,还有char_traits用来保存字符类型相关信息,numeric_limits用来保存数值类型相关信息。

Note:

  • Traits classes 使得“类型相关信息”在编译器可用。它们以 template 和 “template特化”完成实现
  • 整合重载技术后,Traits classes 有可能在编译器对类型执行 if-else测试

条款48:认识 template 元编程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值