萃取和constexpr

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

一、引出萃取

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

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

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

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

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

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

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

template
void func_impl(Iter iter, T t)
{
    T tmp;//这里解决了迭代器所指类型的型别问题
    ...//函数实现
};
template
void func(Iter iter)
{
    func_impl(iter, *iter);
};
int main
{
    vector tmp_v = {1,2,3};
    func(tmp_v.begin());
}

迭代器常用的型别有五种,并不是每一种都可以通过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。

template//template用来处理迭代器型别的信息
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迭代器的设计如下

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
class iterator_traits {
    typedef typename IterT::iterator_category iterator_category;
    ...
};
第二部分:

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

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

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

template
struct iterator_traits
{
    typedef random_access_iterator_tag  iterator_category;
    ...
};

 设计并实现一个iterator_traits:

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

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

template
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
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
    iter += d;
}

template
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag)
{
    if (d>=0)
    {
        while (d--)
        {
            ++iter;
        }
    }
    else
    {
        while(d++)
        {
            --iter;
        }
    }
    
}

template
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
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了:

  • 建立一组重载函数(身份像工人)或函数模板(例如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,可以直接声明内嵌型别。

//该类专门用来萃取迭代器的特性
tempalte
struct itertor_traits
{
    typedef typename I::value_type value_type;
}

这样func可以改写如下

template
typename iterator_traits::value_type
func(I iter){return *iter;}

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

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

tempalte
struct iterator_traits
{
    typedef I value_type;
}

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

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

template
struct iterator_traits//偏特化版,当迭代器是一个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()算法:

template
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。

你可能感兴趣的:(STL源码剖析,C++,1024程序员节,c++,萃取)