Effective C++条款47:模板与泛型编程——请使用traits classes表现类型信息

一、traits机制

以迭代器为演示案例

  • 标准库有一个advance()算法,该算法用来将迭代器移动给定距离,其第一个参数为迭代器,第二个参数为要移动的距离
  • 我们知道迭代器分为5种:
    • Input Iterator:这种迭代器所指的对象,不允许外界改变。只读
    • Output Iterator:只写
    • Forward Iterator:允许“写入型”算法(如replace())在此迭代器所形成的区间上进行读写操作
    • Bidirectional Iterator:可双向移动。某些算法需要逆向遍历某个迭代器区间(例如逆向拷贝某范围内的元素),可以使用这种类型的迭代器
    • Random Access Iterator:这种迭代器涵盖了上面4中迭代器所有的功能
  • 对于上面的五种迭代器,标准库分为定义了下面5个类用来表示五种迭代器:
    • 这些迭代器之间的继承关系是有效的is-a关系
//用于标记迭代器的类型

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 {};
  • 不同的迭代器,其移动方向不同,因此我们的advance()算法需要获取迭代器的类型,然后进行相对应的移动操作。下面是advance()的伪代码形式:

  • 例如: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; //表示迭代器的类型

    };

};
  • 那么我们如何获取迭代器的类型哪?那就是traits技术,它们允许你在编译期间获取某些类型信息
  • 对于迭代器,标准库又定义了一个名为iterator_traits的类,用来萃取迭代器的类型:
//用来处理迭代器的信息

template<typename T>

struct iterator_traits;
  • iterator_traits的定义如下:
    • 其根据传入的迭代器,然后萃取迭代器中的iterator_category成员,然后定义为自己的iterator_category成员
    • iterator_category成员最初是迭代器的类型,因此,iterator_traits中的iterator_category就代表迭代器的类型
template<typename IterT>

struct iterator_traits

{

    //因为iterator_category为数据类型,因此需要用typename

    typedef typename IterT::iterator_category iterator_category;

};
  • 如何萃取:
    • 我们使用迭代器来初始化iterator_traits,例如iterator_traits<IterT>
    • 然后iterator_traits内含有一个名为iterator_category,这个用来表示迭代器IterT的类型
  • 偏特化版本(针对于原始指针):
    • 上面对于用户自定义类型有效,但是如果迭代器是原始指针类型,那就不行了,因为指针没有typedef
    • 因此我们需要对于iterator_traits设计偏特化版本,用来专门处理指针(代码如下)
    • 原始指针与random_access_iterator_tag拥有相同的行为,所以我们为指针类型设置为random_access_iterator_tag别名
template<typename IterT>

struct iterator_traits<IterT*>

{

    //原始指针与random_access_iterator_tag属于同一类型

    typedef random_access_iterator_tag iterator_category;

};

二、设计自己的traits

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

三、traits与重载技术的结合

  • 当有了traits机制之后,我们的advance()算法就可以获取迭代器的类型,并判断做相对应的事情了

错误实现方式

  • 那么一份错误的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))

    {

        ..
    
    }

    else

        //...

}
  • 错误的原因在于:
    • ①这段代码会产生编译器的错误,详细介绍留到条款48介绍
    • ②IterT类型在编译期间获取,iterator_traits<IterT>::iterator_category也可在编译期间确定,但是if语句是在运行期间才会核定。因此将编译期完成的事情延迟到运行期采取确定,那么这不仅浪费时间,也可能造成代码膨胀

正确的做法

  • 有一个方法就是重载,当你重载某函数之后,可以根据你传入的参数来调用不同的版本
  • 因此advance()也是这么实现的,其设计了三个名为三个名为doAdvance()的函数,作为advance()内部调用:
    • 因为forward_iterator_tag继承于input_iterator_tag,因此input_iterator_tag版本的advance()也能够处理forward_iterator_tag迭代器
    • 对于input_iterator_tag来说,其移动负距离会导致不明确行为,因此需要进行检测并进行相对应的错误处理
template<typename IterT,typename DistT>

void doAdvance(IterT& iter, DistT d,std::random_access_iterator_tag)

{

    iter += d;

}


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;

    }

}


template<typename IterT, typename DistT>

void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)

{

    if (d < 0){

        //错误处理

    }

    while (d--)

        ++iter;

}
  • 有了上面的重载函数之后,我们就可以在用户层调用的advance()函数中调用这些重载版本了:
template<typename IterT, typename DistT>

void advance(IterT& iter, DistT d, std::input_iterator_tag)

{

    //萃取迭代器的类型,调用不同的版本

    return doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());

}

四、traits class的总结

  • 使用traits class的总结:
    • 建立一组重载或函数模板(例如doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相对应
    • 建立一个空值函数或函数模板(例如advance),它调用上述那些doAdvance()函数,并传递traits class所提供的信息
  • Traits广泛应用于标准程序库:
    • 其中当然有上述讨论的iterator_traits,除了供应iterator_categoty()还提供了另外四份迭代器相关信息(其中最有用的是value_type,见条款42)
    • 令外还有char_traits用来保存字符类型的相关信息
    • numeric_limits用来保存数值类型的相关信息,例如某数值类型可表现之最小值和最大值等等(此处命名没有采用以traits结尾的风格)

五、总结

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

猜你喜欢

转载自blog.csdn.net/www_dong/article/details/113839276
今日推荐