EffectiveC++-条款47:请使用 traits classes 表现类型信息

本文介绍了如何利用Traits技术处理不同类型的STL迭代器,通过模板和重载实现编译期的类型判断。讨论了迭代器的五种类型及其功能,并展示了如何为不同类型迭代器设计Move函数。通过Traits类和类型信息,避免了运行时的类型检查,提升了效率。
摘要由CSDN通过智能技术生成

一. 内容

  1. 本条款仍然以例子入手,假如你要设计一个 Move 函数,用于将 STL 的迭代器移动指定的距离

    template <typename IteratorType>
    void Move(IteratorType & Iterator,int Distance);`在这里插入代码片`
    
  2. 我们先来认识一下 STL 迭代器的分类,迭代器共有 5 种类型:

    • Input 迭代器,只能一步一步向前移动,只能读取内容,且仅读取一次。
    • Output 迭代器,只能一步一步向前移动,只能写入内容,且仅写入一次。
    • Forward 迭代器,只能一步一步向前移动,可读可写多次。
    • Bidirectianol 迭代器,只能一步一步向前或向后移动,可读可写多次。
    • Random Access 迭代器,可以在常量时间以任意步数向前或向后移动,可读可写多次。

    可以看到,迭代器的功能越来越多,有些功能是前面迭代器不具备的,这意味着当我们编写一个模板函数时,一句 Iterator+=Distance 已经无法满足我们的要求了,有些迭代器是不支持大于1的距离跳跃的,只能反复的进行 ++ 或 --。所以这需要我们根据迭代器的类型判断进行何种操作。

  3. 通过对迭代器的认识,我们知道只有 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;
            }
        }
    }
    

    关键是我们怎么在编译期获得这个参数的类型信息呢?

  4. 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);
    }
    
  5. 现在你应该知道如何设计并实现一个 traits class了:

    • 确认若干你希望将来可取得的类型相关信息。例如迭代器,我们希望获得其分类 IteratorTag。
    • 为该信息选择一个名称。例如 typedef xxx IteratorTag。
    • 提供一个 template 和一组特化版本,内含你希望支持的类型相关信息。例如 typedef typename T::IteratorTag IteratorTag。
  6. 虽然这一切看起来良好,但是存在潜在的编译问题,这点在条款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 技术。

  7. 现在我们可以进一步总结如何使用 traits class了:

    • 建立一组重载函数或函数模板,例如 OnMove,用于对不同的 traits 参数的进行区别实现。
    • 建立一个控制函数或函数模板,例如 Move,调用上述重载函数并传递 traits class 提供的类型信息。
  8. 虽然我们说 traits class,习惯上 traits 总是被实现为 structs,可能考虑到只是简单的传递信息,访问权限默认 public 比较方便

二. 总结

  1. Traits classes 使得类型相关信息在编译器可用。它们以 templates 和 templates 特化完成实现。
  2. 整合重载技术(overloading)后,Traits classes 有可能在编译器对类型执行 if…else 测试。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值