最近重温了一下萃取发现其与constexpr有相似之处,记录如下。
一、引出萃取
STL的在中心思想是将容器和算法分开,再通过迭代器iterator这一迭代器来将两者粘合起来。
通过迭代器进行算法计算,需要涉及两个问题:
问题一.通常需要针对不同类型的迭代器进行不同的算法操作。需要在编译时期获取迭代器的类型信息。
以advance为例,对于random_access_iterator可以在O(1)的时间复杂度完成,但是对于bidirectional_iterator需要在O(n)的时间复杂度完成。
问题二.通常需要运用迭代器的相应型别,相应型别之一就是iterator所指向数据的类型。
C++支持sizeof(),但是不支持typeof()。即使通过RTTI的typeid()获取到类型名称,也不能进行变量声明使用。
解决办法:通过function template的函数推导可以获取到iterator所指向数据的类型。
cpp
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。
这五种型别是:
- value_type
- difference_type
- reference_type
- pointer_type
- iterator_category
Traits不是一种C++关键字或一个预定义的构件。
是一种技术,也是C++程序员需要共同遵守的协议。这个技术的要求之一是,它对内置类型或用户自定义类型的表现必须一样好。
"traits必须能够实施与内置类型"意味着"类型内的嵌套信息"这种东西就出局了,因为我们无法将信息嵌套在原始指针内。因此,类型的traits信息必须位于类型自身之外。
标准技术是把它放入一个template及其一个或多个特化版本中。这样的templates在标准程序库中有若干个,其中针对迭代器的被命名为iterator_traits。
cpp
template<typename T>//template用来处理迭代器型别的信息
struct iterator_traits;
问题一的答案是引入iterator_category;问题二答案是引入value_type。
二、iterator_category和value_type
iterator_category
iterator_traits的运作方式是针对每一个类型的IterT在struct iterator_traits<IterT>中使用typedef声明一个iterator_category。
这个typedef用来确认IterT的迭代器分类。
iterator_traits以两部分实现上述所言:
第一部分:
首先它要求每一个用户自定义的迭代器类型必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构。
例如,deque的迭代器支持随机访问,所以 针对一个deque迭代器的设计如下
cpp
template<...>//略写tempalte参数
class deque
{
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
};
};
list的iterator可以双向前进
cpp
template<...>//略写tempalte参数
class list
{
public:
class iterator {
public:
typedef bidirectional_iterator_tag iterator_category;
};
};
至于iterator_traits只是类似地响应iterator class的嵌套式 typedef:
cpp
template<typename IterT>
class iterator_traits {
typedef typename IterT::iterator_category iterator_category;
...
};
第二部分:
第二部分专门用来应对指针。
上述方法对用户自定义的Iter类型行得通,但是不适用于指针类型,因为指针不可能嵌套typedef。
因为支持指针迭代器,iterator_traits还特别对指针类型提供了一个偏特化版本。由于指针的行径与random_access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:
cpp
template<typename IterT>
struct iterator_traits<IterT*>
{
typedef random_access_iterator_tag iterator_category;
...
};
设计并实现一个iterator_traits:
- 确认若干你希望将来可取得的类型相关信息。例如迭代器而言,我们希望将来可取得其分类。
- 为该消息选择一个名称(例如:iterator_category)。
- 提供一个template和一组特化版本(例如稍早说的itera_traits),内含你希望知道的信息
现在有了itera_traits,我们可以实践先前的advance。
cpp
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT& dist)
{
if (typeid(typename std::iterator_traits<IterT>::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
cpp
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需要做的是调用它们并额外传递一个对象,后者必须带有适当的迭代器分类。于是编译器调用重载解析机制调用适当的实现代码。
cpp
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT& d)
{
doAdvance(iter,d,std::iterator_traits<IterT>::iterator_category);
}
在设计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。比如:
- numeric_limits,数值类型判断
- is_fundamental,判断是否是内置类型
value_type
所谓traits就是如果I有value_type,那么萃取出来的value_tye就是I::value_type,可以直接声明内嵌型别。
cpp
//该类专门用来萃取迭代器的特性
tempalte<typename I>
struct itertor_traits
{
typedef typename I::value_type value_type;
}
这样func可以改写如下
cpp
template<typename I>
typename iterator_traits<I>::value_type
func(I iter){return *iter;}
这样除了多了一层间接性以外还有什么好处呢?
好处就是可以拥有特化版本。现在令iterator_traits拥有一个partial specialization
cpp
tempalte<typename I>
struct iterator_traits<I*>
{
typedef I value_type;
}
于是原生指针int*虽然不是一个class type,仍然可以通过traits来获取其value_type。
如果是一个指向常量对象的原生指针 iterator_traits,希望声明一个可编写的对象。
cpp
template<typename T>
struct iterator_traits<const T*>//偏特化版,当迭代器是一个pointer_to_const时,萃取出来的是T,而不是const T
{
typedef T value_type;
}
总结:
- traits class使得类型信息可以在编译器使用。通过tempalte和tempalte特化完成实现。
- 整合重载技术后,traits class可能在编译器对类型执行if_else测试。(本质上是针对不同类型匹配最恰当的模板。)
三、constexpr与萃取相关联
constexpr可以优化实现编译时if,编译时if的一个典型应用是标记调度。
在c++ 17之前,必须为希望处理的每种类型提供一个重载集,其中每个重载包含一个单独的函数。现在,使用编译时if,可以将所有逻辑放在一个函数中。例如,代替重载std::advance()算法:
cpp
template<typename Iterator, typename Distance>
void advance(Iterator& pos, Distance n)
{
using cat = std::iterator_traits<Iterator>::iterator_category;
if constexpr (std::is_same_v<cat, std::random_access_iterator_tag>)
{
pos += n;
}
else if constexpr (std::is_same_v<cat, std::bidirectional_access_iterator_tag>)
{
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。