如何让算法扩展适用于STL标准

xiaoxiao2021-02-28  60

前言   

     从事过一段时间的程序开发后,对于以后要干什么总算也有了短期的目标。如今的软件行业在互联网至上的大趋势下,很多“程序员”(可能也包括我自己)与其称之为程序员,不如说是码农,大公司大企业封装好了各种框架,特别是各种Java的强大框架让程序开发越来越傻瓜化,在这样的工作环境中,我感觉我反复已经看到了20年后的自己。经历了崇尚各种看起来高大上的各种花哨东西(曾迷恋过各种好看的界面设计)、赏心悦目的设计模(然而大多数时间只用到了单例、工厂)之后,决定重新出发捡起算法,毕竟这才是真正有价值的东西,同时也是为了解决当前的尴尬。     大学里存在这么两种人,一部分人专心学算法做数模,对算法可以说是烂熟,但是编程能力可能不如意。还有一部分人相反,他们编程能力不错,却不喜欢在算法里面打滚。在程序开发中这种情况就十分尴尬,项目组里核心应该是会算法的人,对于解决问题,他们才应该是主力,然而有时候会算法却不一定代表着他们能将之转换为解决方案,或者符合程序设计准则的代码,编程能力强的又不会算法,导致项目停滞。所以说呢,为什么有那句话:两手抓,两手都要硬。这里我会按照常见的算法类型在STL中做出对应的实现,他们的设计符合以下准则:适配于各种STL容器(包括数组)、通过函数对象可对算法进行扩展。

如何让算法扩展适用于STL标准

    按照学习的顺序本应该从最基本的排序开始,但是那些算法太过于简单,甚至没有写下去的必要,所以就不做介绍。我的写作顺序遵循 算法设计与分析基础下 这本书的介绍顺序。在此之前首先要介绍一下STL相关的相关知识,因为要写出符合STL规范的代码就不可避免的要了解它。在此我不得不介绍一本书 STL源码分析 作者候捷,他在这本书里面曾说过,基本任何普通程序员的代码在STL面前都是三流代码。对于初识STL的童鞋来说可能很多停留于vector、map等容器上,没错,这是STL对外的直接表现,但是可能其他的有些点被我们忽略了。    熟悉C#和Java的人可能知道,它们的容器实现实际上实现的是一个个接口,比如ICollection、IList等等,在编程中我们使用它们时会有一致的表现。在C++ STl中我们似乎也是这样,比如我们在使用迭代器的时候大家一定很习惯于写这种代码: vector<int>::iterator map<int,int>::iterator

 

  其实这样的代码看起来就很容易给人一种错觉,那就是:既然vector和map都有一个类型叫iterator,那他们是同一个吗。

   受C#方便的foreach 和 AsEnumerable 枚举的影响,这种想法一开始在我的脑海里挥之不去,在vs的环境中我反反复复的查看源代码(微软的STL实际上是STL的一个版本,作者PJ),并没有找到这类的代码,能看到的全是一个个头大的宏定义。其实作为写程序本身,我是很喜欢宏的,虽然不是宏设计的本意,但是宏的代码替换功能使自动生成代码成为可能,非常方便,大佬们强烈建议不要这么用。对于读代码的人,是最讨厌宏的,因为你看不到它的直观的实现,要一层层去看宏的调用关系。实在没辙之后只好求助于最古老的方式:看源码。微软的STL并不是开源的,但是STL作为一个项目,其实是有相关代码的 SGI STL作为标准的实现其实比PJ的版本更加规范。于是我找来了这本书开始研读。浅尝之后,总算是让我摸到了一点头绪。    我们看起来如此简单的代码,实际上却一点也不简单,如果用术语来概括的话,那应该是叫做:萃取器、内部类型。    内部类型很简单,我们很熟悉的 typedef 做为类的内部使用,就算是内部类型了,萃取器应该是理解上较为复杂的东西。在读过之后我将其概括为C++的一种特产,为什么说是C++的特产,实际上是得益于C++独有的类型推导机制,简而言之是模板中的类型推导机制,可以用来代替做一部分更高维度的继承的工作。这实际上是一种很激动的特性!你可以想象一下以下情形,两个类中共同有一个ID,这个ID表示这个类的对象的唯一性。但是呢A类的ID它是一个int型,B类的ID呢,它是一个string型或者char[],怎样将这种特性加一抽象呢?继承好像对此聪明的同学肯定一定想到了很多解决方案,比如说用使用template模板,实现的形式如下: template <typename T> class base { public: T ID; }     是的这样可以,但是这样又有一个问题,如果我像现在要取得这个ID该怎么做?名字都有了直接取啊。不假思索的方式可能就是这样,但是你在实际写代码的时候你就会感到很尴尬,该怎么写?这样? T obj = base<T>().ID; 是的,针对于这个模板编程的时候你就会发现这个尴尬的事实--无法在外部获得这个值类型。或许你可以针对这个模板的某个实现来进行编程,比如: int id = base<int>().ID; 但是如果我在外部针对于这个模板进行扩展该如何?比如,获取这个对象后,对其id进行处理后返回(func函数)。 template <class T> base { public: T ID; } template <class I> ? func(I base_obj) { ... } 返回值肯定也是模板中传入的T,问题就在于,无法得知其返回值类型、进而无法声明这个函数。 这应该是个很普遍的问题,既然如此普遍那就肯定有解决的办法,前人已经为我们想到了,那就是 内嵌类型。加入内嵌类型后的代码: template <class T> base { public: typedef T value_type; T ID; } template <class I> typename I::value_type func(I base_obj);         当看见这段代码之后,你肯定会觉得,这代码好简单...但是没人写出来你估计一辈子想不出来。实际上,这里还是利用了类型推导机制,外部的函数不清楚这个值的类型,类自己知道啊,那就在类自己内部预先写好,相当于告诉外部函数,你可能需要用到的某个值的类型在这儿~如果要用自己去拿。当 I base_obj传递给func函数后,类型推导机制开始发挥作用,推断出他是一个base<T>类型,返回值就可以进一步根据base<T>去找成员value_type类型,类型推断完成。         如果是你只使用vector map当做容器来使用,而从未想过对其扩展,那么以上内容对你基本没有什么用。但是想要深入了解并扩展这绝对是一个很简单却鲜为人知的写法。为什么要在内部声明很多内部类型?原因就在这里。但是还没完,萃取器可还没用上呢。         为什么叫萃取器,是从其作用上来说的:它可以将符合萃取器约定的类的特性,原汁原味的榨取出来。关键字:符合萃取器约定。还是使用刚才的例子: template <class T> A { public: typedef T value_type; T ID; } template <class T> B { public: typedef T value_type; T ID; } template <class I> struct traits { typedef typename I::value_type value_type; }         如何在不使用模板的情况下,获取A和B的ID类型?        萃取器的写法是: traits<A<T>>::value_type value;//声明了一个A<T>的ID的值类型 traits<B<T>>::value_type value;//声明了一个B<T>的ID的值类型 //T可以显式指定,但是这样没什么必要如traits<B<int>>::value_type value, //还不如直接写int value,但是这样写没有任何问题 //更重要的还是由上文中提到的类型推导机制推断出,适用于编程时不明确的模板类型          traits萃取器,可以看到它只做了一件事,给传进来的 I 模板的内部成员重新取了一个别名,然后就可以直接使用萃取器来进行获取类型的操作,相当于对内部类型的再一次封装。很显然,能使用这个萃取器的模板需要满足一个条件:必须含有内部类型value_type,这也水为什么在上文中特意提到关键字 符合萃取器约定。如此一来就可以做到:只要是有这个内部类型的类,都可以榨取。          这样做有什么意义呢?这就涉及到我们如何适配STL的问题。适配STL的关键在与适配其各种容器,算法不能只支持vector或者只支持某一种数据类型,STL自己的实现是通过迭代器来实现的,比如说我们看这样一个函数sort,它的作用是对指定的数据进行排序: template<class _RanIt, class _Pr> inline void sort(_RanIt _First, _RanIt _Last, _Pr _Pred)         这个声明看起来感觉很可怕,其实应该一部分算是作者PJ的锅。这个参数类型名字取的是在是太抽象了,_RanIt代表的是随机迭代器,_Pr代表的是比较函数,类似于C#的Predicate,但是又不同于它。不同在于实现方式不同,C++的_Predicate是用函数对象的方式实现的,至于何为函数对象请参考其他文章吧,作者是在是精力有限。第一个参数是迭代器的起始位置,第二个是结束。这个函数的作用就很明显到了,对迭代器范围内的元素进行排序。         从这里可以看出STL为了适配自己的容器,算法都时采用的迭代器来进行适配,顺便提一下指针也是一种迭代器哦。如此一来我们的萃取器就派上用场了。为什么呢,因为迭代器遵循了一定的设计规范,而正好萃取器可以萃取这种规范下的约定: template <class Category, class T, class Distance = ptrdiff_t, class Pointer = T*, class Reference = T&> struct iterator { typedef Category iterator_categroy; typedef T value_type; typedef Distance difference_type; typedef Pointer pointer; typedef Reference reference; };        以上是迭代器的设计准则,STL内的任何迭代器都是继承于此。比如可能有名字叫vector_iterator的一个类,专门用来vector的迭代,同理还有map_iterator那么其形式可能是这样: class vector { typedef vector_itertor iterator; } class map { typedef map_itertor iterator; }         这也解决了上面的疑惑,虽然他们都叫iterator,但是其实他们不是一个东西,只是通过内部定义名字相同而已,这样方便利用萃取器进行萃取,迭代器的萃取器的实现方式如下: template <class I> struct iterator_traits { typedef typename I::iterator_categroy iterator_categroy; typedef typename I::value_type value_type; typedef typename I::difference_type difference_type; typedef typename I::pointer pointer; typedef typename I::reference reference; };          同样也是对迭代器的内部类型进行了再一次的封装。但是这里又要遇到另一个问题,如何处理原生指针?因为原生指针也是一种迭代器,二指针作为模板传入这个萃取器是不能工作的,因为指针并没有内部类型定义为 iterator_categroy等的类型。神奇的是STL内部的算法是可以接受指针为迭代器的,难道我们的设计思路是错的?很显然不是,这里又设计到另一种技术,叫做特化和偏特化,作用可以理解为为这个模板做出某种特定情况下的实现,比如这里想对指针做出不同的处理,就可以使用,关于特化和偏特化的内容,请阅读其他有关的文章了解吧,作者就不再写了,因为比较简单,这里给出指针的常量指针的萃取器实现。需要指出的是这些代码在STL中都有类似的实现,但是在各个版本的STL中可能名称不一,如果想要使用萃取器的特性,最好自己编写一份,以供使用,在后续文章中我会给出萃取器的完整实现,感兴趣的可以持续关注。 template <class T> struct iterator_traits<T*> { typedef T value_type; typedef ptrdiff_t difference_type; typedef T* pointer_type; typedef T& reference_type; typedef random_access_iterator_tag iterator_categroy; }; // partial specializations of value_type for original const pointer template <class T> struct iterator_traits<const T*> { typedef T value_type; // not const T typedef ptrdiff_t difference_type; typedef const T* pointer_type; typedef const T& reference_type; typedef random_access_iterator_tag iterator_category; };          至此,如何将算法适配STL的原理已经介绍完毕,接下来我会以归并排序这一例子来具体说明萃取器在这里面发挥的作用。
转载请注明原文地址: https://www.6miu.com/read-36123.html

最新回复(0)