最近重温了一下萃取发现其与constexpr有相似之处,记录如下。
STL的在中心思想是将容器和算法分开,再通过迭代器iterator这一迭代器来将两者粘合起来。
通过迭代器进行算法计算,需要涉及两个问题:
问题一.通常需要针对不同类型的迭代器进行不同的算法操作。需要在编译时期获取迭代器的类型信息。
以advance为例,对于random_access_iterator可以在O(1)的时间复杂度完成,但是对于bidirectional_iterator需要在O(n)的时间复杂度完成。
问题二.通常需要运用迭代器的相应型别,相应型别之一就是iterator所指向数据的类型。
C++支持sizeof(),但是不支持typeof()。即使通过RTTI的typeid()获取到类型名称,也不能进行变量声明使用。
解决办法:通过function template的函数推导可以获取到iterator所指向数据的类型。
- template<typename Iter, typename T>
- void func_impl(Iter iter, T t)
- {
- T tmp;//这里解决了迭代器所指类型的型别问题
- ...//函数实现
- };
- template<typename Iter>
- void func(Iter iter)
- {
- func_impl(iter, *iter);
- };
- int main
- {
- vector<int> tmp_v = {1,2,3};
- func(tmp_v.begin());
- }
迭代器常用的型别有五种,并不是每一种都可以通过template的参数推导机制获取,我们需要更全面的解法,即traits。
这五种型别是:
Traits不是一种C++关键字或一个预定义的构件。
是一种技术,也是C++程序员需要共同遵守的协议。这个技术的要求之一是,它对内置类型或用户自定义类型的表现必须一样好。
“traits必须能够实施与内置类型”意味着“类型内的嵌套信息”这种东西就出局了,因为我们无法将信息嵌套在原始指针内。因此,类型的traits信息必须位于类型自身之外。
标准技术是把它放入一个template及其一个或多个特化版本中。这样的templates在标准程序库中有若干个,其中针对迭代器的被命名为iterator_traits。
- template<typename T>//template用来处理迭代器型别的信息
- struct iterator_traits;
问题一的答案是引入iterator_category;问题二答案是引入value_type。
iterator_traits的运作方式是针对每一个类型的IterT在struct iterator_traits
这个typedef用来确认IterT的迭代器分类。
iterator_traits以两部分实现上述所言:
首先它要求每一个用户自定义的迭代器类型必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构。
例如,deque的迭代器支持随机访问,所以 针对一个deque迭代器的设计如下
- template<...>//略写tempalte参数
- class deque
- {
- public:
- class iterator {
- public:
- typedef random_access_iterator_tag iterator_category;
- };
- };
list的iterator可以双向前进
- template<...>//略写tempalte参数
- class list
- {
- public:
- class iterator {
- public:
- typedef bidirectional_iterator_tag iterator_category;
- };
- };
至于iterator_traits只是类似地响应iterator class的嵌套式 typedef:
- template<typename IterT>
- class iterator_traits {
- typedef typename IterT::iterator_category iterator_category;
- ...
- };
第二部分专门用来应对指针。
上述方法对用户自定义的Iter类型行得通,但是不适用于指针类型,因为指针不可能嵌套typedef。
因为支持指针迭代器,iterator_traits还特别对指针类型提供了一个偏特化版本。由于指针的行径与random_access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:
- template<typename IterT>
- struct iterator_traits
- {
- typedef random_access_iterator_tag iterator_category;
- ...
- };
设计并实现一个iterator_traits:
现在有了itera_traits,我们可以实践先前的advance。
- template<typename IterT, typename DistT>
- void advance(IterT& iter, DistT& dist)
- {
- if (typeid(typename std::iterator_traits
::iterator_category) - == typeid(std::random_access_iterator_tag))
- {
- //直接加dist
- iter += dist;
- }
- else
- {
- //逐个++或--
- }
- }
虽然看起来没有问题,但是编译有问题(当传入的Iter不支持直接算术加法的时候(+=)编译就会有问题,即使我们只知道代码绝对不会执行到+=这里,但是编译器必须保证所有代码都有效)。
IterT类型在编译期间获知,所以iterator_traits::category也可以在编译期确定。但是if是在运行的时候才核定。
为什么将可以在编译器完成的事情放在运行期才做?这不仅浪费时间还会导致代码膨胀。
我们真正想要的是一个条件式判断“编译器核定成功类型”。恰巧C++有一个取得这种行为的办法,那就是重载。
当你重载某个函数f,你必须详细叙述各个重载件的参数类型。当你调用f,编译器便根据传来的实参选择最适当的重载件。
为了能够产生针对类型的”编译器条件“,我们需要两版重载函数,内含advance的本质内容,但各自接收不同类型的iterator_category对象。新函数取名为doAdvance
- template<typename IterT& iter, typename DistT>
- void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
- {
- iter += d;
- }
-
- template<typename IterT& iter, 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& iter, 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,d,std::iterator_traits
::iterator_category); - }
在设计iterator的时候我们必须尽可能针对某种迭代器提供一个明确的定义,针对强化的某种迭代器提供另一种定义,这样才能在不同情况下提供最大的效率。
STL研究中时刻铭记在心的就是效率问题。当有个算法可以接收FowardIterator,而我们提供给他一个RandomAccessIterator,算法可以执行,但可用不代表最佳!
注意每个_advance函数的最后一个参数只是声明型别,并没有指定参数名称,因为它纯粹是为了激活重载机制,函数之中根本不使用参数,硬加参数也不过是化蛇添足罢了。
一个迭代器的型别其类型永远落在“该迭代器所属各类型中最强化的那个”,例如int*既是RandomAccessIterator,又是Bidrectional Iterator,还是Forward Iterator,也是Input Iterator。
我们现在可以总结如何使用一个traits class了:
TR1总共为C++提供了50多个traits class。比如:
所谓traits就是如果I有value_type,那么萃取出来的value_tye就是I::value_type,可以直接声明内嵌型别。
- //该类专门用来萃取迭代器的特性
- tempalte<typename I>
- struct itertor_traits
- {
- typedef typename I::value_type value_type;
- }
这样func可以改写如下
- template<typename I>
- typename iterator_traits::value_type
- func(I iter){return *iter;}
这样除了多了一层间接性以外还有什么好处呢?
好处就是可以拥有特化版本。现在令iterator_traits拥有一个partial specialization
- tempalte<typename I>
- struct iterator_traits
- {
- typedef I value_type;
- }
于是原生指针int*虽然不是一个class type,仍然可以通过traits来获取其value_type。
如果是一个指向常量对象的原生指针 iterator_traits,希望声明一个可编写的对象。
- template<typename T>
- struct iterator_traits<const T*>//偏特化版,当迭代器是一个pointer_to_const时,萃取出来的是T,而不是const T
- {
- typedef T value_type;
- }
总结:
constexpr可以优化实现编译时if,编译时if的一个典型应用是标记调度。
在c++ 17之前,必须为希望处理的每种类型提供一个重载集,其中每个重载包含一个单独的函数。现在,使用编译时if,可以将所有逻辑放在一个函数中。例如,代替重载std::advance()算法:
- template<typename Iterator, typename Distance>
- void advance(Iterator& pos, Distance n)
- {
- using cat = std::iterator_traits
::iterator_category; - if constexpr (std::is_same_v
) - {
- pos += n;
- }
- else if constexpr (std::is_same_v
) - {
- if (n >= 0)
- {
- while (n--)
- {
- ++pos;
- }
- }
- else
- {
- while (n++)
- {
- --pos;
- }
- }
- }
- else // input_iterator_tag
- {
- while (n--)
- {
- ++pos;
- }
- }
- }
注意:这里使用的是is_same_v(is_same的辅助模板),而不是typeid(运算符)。因为typeid一般是执行期动态获取,除非内置类型是静态获取;而且if constexpr需要常量,在编译期就获取,因此这里不能使用typeid。