C++ STL 源码阅读 (四): sort

qsort vs std::sort

朋友问我,qsort和std::sort有什么区别,我没有专门查过,但还是尝试答了几条:

  1. qsort是C标准库函数,位于<stdlib.h>;sort是STL中的函数模板,位于<algorithm>
  2. qsort的参数用指针表示范围;sort的参数用迭代器表示范围
  3. qsort肯定是快排,sort应该是根据迭代器类型来判断是否采用快排,如果是前向迭代器的话应该就不是快排

第三条是我猜的,后来查过资料之后,发现我第三条确实答错了,事实上:

  • sort的迭代器参数只能是Mutable RandomAccessIterator。
  • sort的排序算法不是快排,而是IntroSort和InsertionSort的结合。(内省排序 和 插入排序)

下面说一下std::sort的源码,以及记录一下阅读中遇到的不懂的东西(不懂的实在有点多,可能有点啰嗦)。

std::sort

std::sort有两个重载:

  template<typename _RAIter>
    void 
    sort(_RAIter, _RAIter);

  template<typename _RAIter, typename _Compare>
    void 
    sort(_RAIter, _RAIter, _Compare);

分别点进去:

  template<typename _RandomAccessIterator>
    inline void
    sort(_RandomAccessIterator __first, _RandomAccessIterator __last)
    {
      // concept requirements
      __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
        _RandomAccessIterator>)
      __glibcxx_function_requires(_LessThanComparableConcept<
        typename iterator_traits<_RandomAccessIterator>::value_type>)
      __glibcxx_requires_valid_range(__first, __last);

      std::__sort(__first, __last, __gnu_cxx::__ops::__iter_less_iter());
    }

  template<typename _RandomAccessIterator, typename _Compare>
    inline void
    sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
     _Compare __comp)
    {
      // concept requirements
      __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
        _RandomAccessIterator>)
      __glibcxx_function_requires(_BinaryPredicateConcept<_Compare,
        typename iterator_traits<_RandomAccessIterator>::value_type,
        typename iterator_traits<_RandomAccessIterator>::value_type>)
      __glibcxx_requires_valid_range(__first, __last);

      std::__sort(__first, __last, __gnu_cxx::__ops::__iter_comp_iter(__comp));
    }

殊途同归,调用了同一个函数std::__sort,只是第三个参数不同。

接下来先不看std::__sort的源码,先搞懂调用该函数前的那一堆代码是什么。

__glibcxx_function_requires

上面代码里面,__glibcxx_function_requires是宏定义,涉及到c++ concept checking的概念。

显然concept checking不是C++11标准的一部分,它是Boost的一部分,是Boost Concept Checking Library的内容,然后GNU将其整合到了 GNU C++ library中。gcc里面,concept checking默认是关闭的,可以通过 #define _GLIBCXX_CONCEPT_CHECKS 来打开它,但是没有任何必要,因为C++11的特性会完成concept checking的功能。更具体的解释见文末的引用[6][7]。

简而言之,这个concept check的作用是“限制某类型的特定的操作是合法的”。比如说,__glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<_RandomAccessIterator>)就是要限制你传入的迭代器类型得是Mutable RandomAccessIterator,得支持++ -- [] * += -= 等运算符操作。如果不满足的话,编译就不通过。

上面的sort(__first, __last)里的两条__glibcxx_function_requires就限制了:

  • 迭代器类型得是Mutable RandomAccessIterator
  • 迭代器所指类型得是支持<运算符的

之前我只清楚iterator的五种类型,即std::input_iterator_tag等代表的五种,然后我就有了以下疑问:

  1. 为什么用concept check来检查迭代器类型?像std::advance那样用__iterator_traits检查迭代器类型不行吗?
  2. 迭代器类型中有mutable iterator吗?五种迭代器类型好像没有提到啊?

针对问题查了cppreference.com [4]:

There are five (until C++17)six (since C++17) kinds of iterators: InputIterator, OutputIterator, ForwardIterator, BidirectionalIterator, RandomAccessIterator, and ContiguousIterator (since C++17).

All of the iterator categories (except OutputIterator and ContiguousIterator) can be organized into a hierarchy, where more powerful iterator categories (e.g. RandomAccessIterator) support the operations of less powerful categories (e.g. InputIterator). If an iterator falls into one of these categories and also satisfies the requirements of OutputIterator, then it is called a mutable iterator and supports both input and output. Non-mutable iterators are called constant iterators.

mutable iterator指的支持write和increment的迭代器。因此,iterator_catogory是random_access_iterator_tag的迭代器并不一定可写,random_access_iterator_tag并不能保证迭代器是mutable还是constant,只有mutable iterator才是可写的。所以上面的两个问题有答案了。

那么concept check是怎么实现的呢?接下来接着看源码。

点进去__glibcxx_function_requires:

#ifndef _GLIBCXX_CONCEPT_CHECKS
#define __glibcxx_function_requires(...)
#else // the checks are on
#define __glibcxx_function_requires(...)                                 \
            __gnu_cxx::__function_requires< __gnu_cxx::__VA_ARGS__ >();
#endif // enable/disable

因此,如果想要使用concept check,就需要自己在include相关头文件之前定义宏_GLIBCXX_CONCEPT_CHECKS,或者修改c++config.h中的宏定义。

接下来看 __gnu_cxx::__function_requires。

template <class _Concept>
inline void __function_requires()
{
  void (_Concept::*__x)() _IsUnused = &_Concept::__constraints;
}

_IsUnused是宏定义,展开后是__attribute__((unused)),表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息。如果看不懂__function_requires里面的语法,请参考引用[2][3]。

所以,最初std::sort里的第一个__glibcxx_function_requires展开之后就是:

__gnu_cxx::__function_requires< __gnu_cxx::_Mutable_RandomAccessIteratorConcept<
        _RandomAccessIterator>>();

然而这做了什么?

首先,这句话导致实例化并调用了__function_requires模板函数。该函数里,定义了一个函数指针,指向模板参数表示的类的__constraints成员函数,进而导致实例化了__gnu_cxx::_Mutable_RandomAccessIteratorConcept<_RandomAccessIterator>::__constraints。在__constraints函数中会对迭代器进行某些运算符操作,编译时编译器会检查这些操作的有效性,因此,就达到了检查迭代器性质的作用。

贴出来_Mutable_RandomAccessIteratorConcept的源码,加以印证:

  template <class _Tp>
  struct _Mutable_RandomAccessIteratorConcept
  {
    void __constraints() {
      __function_requires< _RandomAccessIteratorConcept<_Tp> >();
      __function_requires< _Mutable_BidirectionalIteratorConcept<_Tp> >();
      __i[__n] = *__i;                  // require element access and assignment
    }
    _Tp __i;
    typename std::iterator_traits<_Tp>::difference_type __n;
  };

现在std::__sort之前的那一大坨我们已经弄清楚了,接下来进入std::__sort。

std::__sort

接下来std::__sort的实现和侯捷的《STL源码剖析》中讲的std::sort的实现基本一样了。我看的这个STL版本是gcc 5.3.1使用的版本,其std::sort的实现只是在侯捷书中讲的版本上套了个壳子,加了concept checking(也就是前面大费周章讲的东西)。如果有这本书的话可以去看这本书,书上讲的详细多了,这里只简单介绍一下实现。

std::__sort的排序算法是IntroSort和InsertionSort的结合,先说一下什么是IntroSort。

这个排序算法类似于快排,在快排的基础上,当递归深度超过一定深度(深度为排序元素数量的对数值)时转为堆排序。伪代码如下:

procedure sort(A : array):
    let maxdepth = ⌊log(length(A))⌋ × 2
    introsort(A, maxdepth)

procedure introsort(A, maxdepth):
    n ← length(A)
    p ← partition(A)  // assume this function does pivot selection, p is the final position of the pivot
    if n ≤ 1:
        return  // base case
    else if maxdepth = 0:
        heapsort(A)
    else:
        introsort(A[0:p], maxdepth - 1)
        introsort(A[p+1:n], maxdepth - 1)

接下来贴出来std::__sort源码,我把代码加上了注释,不再单独解释代码了。

 template<typename _RandomAccessIterator, typename _Compare>
    inline void
    __sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
       _Compare __comp)
    {
      if (__first != __last)
    {
      // __introsort_loop先进行一遍IntroSort,但是不是严格意义上的IntroSort。
      //因为执行完之后区间并不是完全有序的,而是基本有序的。
      //__introsort_loop和IntroSort不同的地方是,__introsort_loop会在一开始会判断区间的大小,当区间小于16的时候,就直接返回。
      std::__introsort_loop(__first, __last,
                std::__lg(__last - __first) * 2,
                __comp); 
      // 在区间基本有序的基础上再做一遍插入排序,使区间完全有序
      std::__final_insertion_sort(__first, __last, __comp);
    }
    }

__introsort_loop和__final_insertion_sort代码:


  enum { _S_threshold = 16 };

  template<typename _RandomAccessIterator, typename _Size, typename _Compare>
    void
    __introsort_loop(_RandomAccessIterator __first,
             _RandomAccessIterator __last,
             _Size __depth_limit, _Compare __comp)
    {
      // 若区间大小<=16就不再排序。
      while (__last - __first > int(_S_threshold))
    {
      // 若递归次数达到限制,就改用堆排序
      if (__depth_limit == 0)
        {
          std::__partial_sort(__first, __last, __last, __comp);
          return;
        }
      --__depth_limit;
      _RandomAccessIterator __cut =
      std::__unguarded_partition_pivot(__first, __last, __comp); // 分割
      std::__introsort_loop(__cut, __last, __depth_limit, __comp); // 右半区间递归
      __last = __cut;
    // 回到while循环,对左半区间进行排序,这么做能显著减少__introsort_loop的调用的次数
    }
    }


  template<typename _RandomAccessIterator, typename _Compare>
    void
    __final_insertion_sort(_RandomAccessIterator __first,
               _RandomAccessIterator __last, _Compare __comp)
    {
      if (__last - __first > int(_S_threshold)) // 区间长度大于16
    {
      // 插入排序
      std::__insertion_sort(__first, __first + int(_S_threshold), __comp); 
      // 也是插入排序,只是在插入排序的内循环时,不再判断边界条件,因为已经保证了区间前面肯定有比待插入元素更小的元素
      std::__unguarded_insertion_sort(__first + int(_S_threshold), __last, 
                      __comp);
    }
      else // 区间长度小于等于16的话
    std::__insertion_sort(__first, __last, __comp); // 插入排序
    }

__unguarded_insertion_sort和__insertion_sort和__unguarded_partition_pivot不再赘述。

总结

感觉讲了一堆废话,C++11中concept checking都不用了,感觉白看了......

Reference

  1. how does __glibcxx_function_requires and __glibcxx_requires_valid_range macros work?
  2. Function pointer to member function
  3. function pointers in c++ : error: must use '.' or '->' to call pointer-to-member function in function
  4. https://en.cppreference.com/w/cpp/iterator
  5. https://en.wikipedia.org/wiki/Introsort
  6. Compile Time Checks
  7. Using Concept Checks
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 青草花相间,菩提一指来。 风吹花草静,不知花何明。
    任岸阅读 142评论 0 0
  • 香帅 第十五周
    Aero小白阅读 86评论 0 0
  • 2017年1月24日晚,网友@琳哒是我 在微博发帖称,2011年11月11日,她在丽江游玩时被一群男子无端殴打,伤...
    废宅小鱼儿阅读 550评论 2 6