• 萃取和constexpr


     最近重温了一下萃取发现其与constexpr有相似之处,记录如下。

    一、引出萃取

    STL的在中心思想是将容器和算法分开,再通过迭代器iterator这一迭代器来将两者粘合起来。

    通过迭代器进行算法计算,需要涉及两个问题:

    问题一.通常需要针对不同类型的迭代器进行不同的算法操作。需要在编译时期获取迭代器的类型信息。

    以advance为例,对于random_access_iterator可以在O(1)的时间复杂度完成,但是对于bidirectional_iterator需要在O(n)的时间复杂度完成。

    问题二.通常需要运用迭代器的相应型别,相应型别之一就是iterator所指向数据的类型。

    C++支持sizeof(),但是不支持typeof()。即使通过RTTI的typeid()获取到类型名称,也不能进行变量声明使用。

    解决办法:通过function template的函数推导可以获取到iterator所指向数据的类型。

    1. template<typename Iter, typename T>
    2. void func_impl(Iter iter, T t)
    3. {
    4. T tmp;//这里解决了迭代器所指类型的型别问题
    5. ...//函数实现
    6. };
    7. template<typename Iter>
    8. void func(Iter iter)
    9. {
    10. func_impl(iter, *iter);
    11. };
    12. int main
    13. {
    14. vector<int> tmp_v = {1,2,3};
    15. func(tmp_v.begin());
    16. }

    迭代器常用的型别有五种,并不是每一种都可以通过template的参数推导机制获取,我们需要更全面的解法,即traits。

    这五种型别是:

    1. value_type
    2. difference_type
    3. reference_type
    4. pointer_type
    5. iterator_category

    Traits不是一种C++关键字或一个预定义的构件。

    是一种技术,也是C++程序员需要共同遵守的协议。这个技术的要求之一是,它对内置类型或用户自定义类型的表现必须一样好。

    “traits必须能够实施与内置类型”意味着“类型内的嵌套信息”这种东西就出局了,因为我们无法将信息嵌套在原始指针内。因此,类型的traits信息必须位于类型自身之外。

    标准技术是把它放入一个template及其一个或多个特化版本中。这样的templates在标准程序库中有若干个,其中针对迭代器的被命名为iterator_traits。

    1. template<typename T>//template用来处理迭代器型别的信息
    2. struct iterator_traits;

    问题一的答案是引入iterator_category;问题二答案是引入value_type。

    二、iterator_category和value_type

    iterator_category

    iterator_traits的运作方式是针对每一个类型的IterT在struct iterator_traits中使用typedef声明一个iterator_category。
    这个typedef用来确认IterT的迭代器分类。

    iterator_traits以两部分实现上述所言:

    第一部分:

    首先它要求每一个用户自定义的迭代器类型必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构。

    例如,deque的迭代器支持随机访问,所以 针对一个deque迭代器的设计如下

    1. template<...>//略写tempalte参数
    2. class deque
    3. {
    4. public:
    5. class iterator {
    6. public:
    7. typedef random_access_iterator_tag iterator_category;
    8. };
    9. };

     list的iterator可以双向前进

    1. template<...>//略写tempalte参数
    2. class list
    3. {
    4. public:
    5. class iterator {
    6. public:
    7. typedef bidirectional_iterator_tag iterator_category;
    8. };
    9. };

     至于iterator_traits只是类似地响应iterator class的嵌套式 typedef:

    1. template<typename IterT>
    2. class iterator_traits {
    3. typedef typename IterT::iterator_category iterator_category;
    4. ...
    5. };
    第二部分:

    第二部分专门用来应对指针。

    上述方法对用户自定义的Iter类型行得通,但是不适用于指针类型,因为指针不可能嵌套typedef。

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

    1. template<typename IterT>
    2. struct iterator_traits
    3. {
    4. typedef random_access_iterator_tag iterator_category;
    5. ...
    6. };

     设计并实现一个iterator_traits:

    • 确认若干你希望将来可取得的类型相关信息。例如迭代器而言,我们希望将来可取得其分类。
    • 为该消息选择一个名称(例如:iterator_category)。
    • 提供一个template和一组特化版本(例如稍早说的itera_traits),内含你希望知道的信息

    现在有了itera_traits,我们可以实践先前的advance。

    1. template<typename IterT, typename DistT>
    2. void advance(IterT& iter, DistT& dist)
    3. {
    4. if (typeid(typename std::iterator_traits::iterator_category)
    5. == typeid(std::random_access_iterator_tag))
    6. {
    7. //直接加dist
    8. iter += dist;
    9. }
    10. else
    11. {
    12. //逐个++或--
    13. }
    14. }

    虽然看起来没有问题,但是编译有问题(当传入的Iter不支持直接算术加法的时候(+=)编译就会有问题,即使我们只知道代码绝对不会执行到+=这里,但是编译器必须保证所有代码都有效)。

    IterT类型在编译期间获知,所以iterator_traits::category也可以在编译期确定。但是if是在运行的时候才核定。

    为什么将可以在编译器完成的事情放在运行期才做?这不仅浪费时间还会导致代码膨胀。

    我们真正想要的是一个条件式判断“编译器核定成功类型”。恰巧C++有一个取得这种行为的办法,那就是重载。

    当你重载某个函数f,你必须详细叙述各个重载件的参数类型。当你调用f,编译器便根据传来的实参选择最适当的重载件。

    为了能够产生针对类型的”编译器条件“,我们需要两版重载函数,内含advance的本质内容,但各自接收不同类型的iterator_category对象。新函数取名为doAdvance

    1. template<typename IterT& iter, typename DistT>
    2. void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
    3. {
    4. iter += d;
    5. }
    6. template<typename IterT& iter, typename DistT>
    7. void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag)
    8. {
    9. if (d>=0)
    10. {
    11. while (d--)
    12. {
    13. ++iter;
    14. }
    15. }
    16. else
    17. {
    18. while(d++)
    19. {
    20. --iter;
    21. }
    22. }
    23. }
    24. template<typename IterT& iter, typename DistT>
    25. void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
    26. {
    27. if(d<0)
    28. {
    29. throw std::out_of_range("Negative distance");
    30. }
    31. while(d--) ++iter;
    32. }

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

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

    1. template<typename IterT, typename DistT>
    2. void advance(IterT& iter, DistT& d)
    3. {
    4. doAdvance(iter,d,std::iterator_traits::iterator_category);
    5. }

    在设计iterator的时候我们必须尽可能针对某种迭代器提供一个明确的定义,针对强化的某种迭代器提供另一种定义,这样才能在不同情况下提供最大的效率。

    STL研究中时刻铭记在心的就是效率问题。当有个算法可以接收FowardIterator,而我们提供给他一个RandomAccessIterator,算法可以执行,但可用不代表最佳!

    注意每个_advance函数的最后一个参数只是声明型别,并没有指定参数名称,因为它纯粹是为了激活重载机制,函数之中根本不使用参数,硬加参数也不过是化蛇添足罢了。

     一个迭代器的型别其类型永远落在“该迭代器所属各类型中最强化的那个”,例如int*既是RandomAccessIterator,又是Bidrectional Iterator,还是Forward Iterator,也是Input Iterator。

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

    • 建立一组重载函数(身份像工人)或函数模板(例如doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现与其接收之traits相应和。
    • 建立可以控制函数(身份像是工头)或函数模板(例如advance),它调用上述的“工人函数”并传递traits class所提供的信息。

     TR1总共为C++提供了50多个traits class。比如:

    1. numeric_limits,数值类型判断
    2. is_fundamental,判断是否是内置类型

    value_type

    所谓traits就是如果I有value_type,那么萃取出来的value_tye就是I::value_type,可以直接声明内嵌型别。

    1. //该类专门用来萃取迭代器的特性
    2. tempalte<typename I>
    3. struct itertor_traits
    4. {
    5. typedef typename I::value_type value_type;
    6. }

    这样func可以改写如下

    1. template<typename I>
    2. typename iterator_traits::value_type
    3. func(I iter){return *iter;}

     这样除了多了一层间接性以外还有什么好处呢?

    好处就是可以拥有特化版本。现在令iterator_traits拥有一个partial specialization

    1. tempalte<typename I>
    2. struct iterator_traits
    3. {
    4. typedef I value_type;
    5. }

    于是原生指针int*虽然不是一个class type,仍然可以通过traits来获取其value_type。

    如果是一个指向常量对象的原生指针 iterator_traits,希望声明一个可编写的对象。

    1. template<typename T>
    2. struct iterator_traits<const T*>//偏特化版,当迭代器是一个pointer_to_const时,萃取出来的是T,而不是const T
    3. {
    4. typedef T value_type;
    5. }

    总结:

    • traits class使得类型信息可以在编译器使用。通过tempalte和tempalte特化完成实现。
    • 整合重载技术后,traits class可能在编译器对类型执行if_else测试。(本质上是针对不同类型匹配最恰当的模板。)

    三、constexpr与萃取相关联

    constexpr可以优化实现编译时if,编译时if的一个典型应用是标记调度。

    在c++ 17之前,必须为希望处理的每种类型提供一个重载集,其中每个重载包含一个单独的函数。现在,使用编译时if,可以将所有逻辑放在一个函数中。例如,代替重载std::advance()算法:

    1. template<typename Iterator, typename Distance>
    2. void advance(Iterator& pos, Distance n)
    3. {
    4. using cat = std::iterator_traits::iterator_category;
    5. if constexpr (std::is_same_v)
    6. {
    7. pos += n;
    8. }
    9. else if constexpr (std::is_same_v)
    10. {
    11. if (n >= 0)
    12. {
    13. while (n--)
    14. {
    15. ++pos;
    16. }
    17. }
    18. else
    19. {
    20. while (n++)
    21. {
    22. --pos;
    23. }
    24. }
    25. }
    26. else // input_iterator_tag
    27. {
    28. while (n--)
    29. {
    30. ++pos;
    31. }
    32. }
    33. }

    注意:这里使用的是is_same_v(is_same的辅助模板),而不是typeid(运算符)。因为typeid一般是执行期动态获取,除非内置类型是静态获取;而且if constexpr需要常量,在编译期就获取,因此这里不能使用typeid。

  • 相关阅读:
    centos/rocky/redat 8 删除swap分区,重启后无法进入系统
    模型压缩-对模型结构进行优化
    关于“网络安全”五点须知!
    Vue3(2):Vue3使用socket.io
    Java中的关键字super
    ios app开发环境搭建
    搞个微信小程序001
    Window系统安装Nacos
    数据分析 - 离散概率分布的运用
    不使用AMap.DistrictSearch,通过poi数据绘制省市县区块
  • 原文地址:https://blog.csdn.net/ThorKing01/article/details/134020507