从零开始的 STL 实现记录:Algorithm-3
前言
本节将会分析 algobase.h
中的算法,algobase.h
中存放的都是一些比较基础的算法,如 max()
、min()
、copy()
等,这里就挑其中最具有代表性的 copy()
函数来分析,除了 copy()
外还有许多如 copy_backward()
、move()
、move_backward()
等函数,STL 对这些函数效率上的优化方式与 copy()
的处理基本是一致的,就不再分析。
copy
不论是对客端程序或对 STL 内部而言,copy()
都是一个常常被调用的函数。由于 copy 进行的是复制操作,而复制操作不外乎运用 assignment operator
或 copy constructor
(copy算法用的是前者),但是某些元素型别拥有的是 trivialassignment operator
,因此,如果能够使用内存直接复制行为(例如标准函数 memmove
或 memcpy
),便能够节省大量时间。为此,SGI STL的copy 算法用尽各种办法,包括函数重载(function overloading)、型别特性(type traits)、偏特化(partial specialization)等编程技巧,无所不用其极地加强效率。下图展示了整个 copy()
操作的脉络。
从上面的流程可以大概梳理出 copy()
的调用脉络。
首先,copy
函数本身除了一个模板函数的泛化版本,还为 char*
以及 wchar_t*
提供了两个特化版本,针对这两种类型的 copy
直接调用 memmove()
,其余类型元素的 copy
均转调用 __copy_dispatch
;
__copy_dispath
有一个完全泛化版本以及两个针对原生指针 T*
和 const T*
的偏特化版本;
- 完全泛化版本的
__copy_dispatch
根据输入的迭代器类型转调用__copy
,__copy
有处理input_iterator
与random_access_iterator
的两个重载,均会针对迭代器类型采取最有效率的措施。 - 针对原生指针的
__copy_dispatch
会根据指针所指元素的is_trivially_copy_assignable
特性来决定拷贝策略,若为平凡类型则直接调用memmove()
,否则转调用__copy_d
。
可以看到 STL 中的 copy
函数的实现确实复杂,通过函数重载、偏特化、型别特性等一系列手段对不同的类别采取不同的方法,但这也是为了效率的最优,it deserves。
上面这样说明还是有些抽象,下面列出 TinySTL
中实现的代码并且在每个函数中都加入了输入,配合测试案例一起理解会清晰许多。
MyTinySTL
中对于algobase.h
中函数的具体实现与 STL 中还是有不小的差距,因此copy
这部分还是遵循 STL 的逻辑来实现与分析。
1 | /// @brief 完全泛化版本 copy |
测试案例
下面的案例中,代码底部的注释即为运行的输出。
1 | tinystl::vector<int> v1{1, 2, 3, 4, 5}; |
上面的输出还是很耐人寻味的。
首先,为什么明明是 vector<int>
调用的却是 T*
的 copy
方法?答案其实比较明显了,在 vector 容器中说过,vector 的迭代器就是普通的指针,因此这里确实会调用 __copy_t
方法进行拷贝,合理。
其次,为什么调用了两次 copy
?这其实是 vector 的初始化列表构造的原因,vector 的初始化列表构造会调用 uninitialized_copy()
,从而间接调用 copy()
,但这只是在 trivally_copy_assignable
类型的情况下。
当我们不使用初始化列表构造时,就只会输出一次 copy
的信息了。
1 | tinystl::vector<int> v1(5); |
我们再定义一个 non-trivial
的类型 A 并进行测试,发现 copy
也会调用相应的版本。
这里有一点比较奇怪,虽然说 SGI-STL 中对于所有的用户自定义类型都采用了最保守的 type traits,但不同版本的 STL 貌似策略并不相同,而且在
TinySTL
中使用的是诸如std::is_trivially_copy_assignable<T>
来判断 type traits,所以运行结果会与预期的有所差别。
1 | class A { |
换一个不是迭代器不为原生指针的容器类型,如 deque,再进行测试,可以发现 copy 也会转调用 random_access_iterator
版本的 __copy_dispatch
重载。
1 | tinystl::deque<int> d1(5); |
最后是 char*
类型,copy
直接在最外层针对该类型做了特化,不会进一步向下调用。
1 | const char s1[] = "hello"; |
总结
这一节简单分析了 algobase.h
中最具有代表性的一个函数:copy
,该函数通过各种精妙的设计来实现运行时的效率最优,很好地体现了 STL 中函数的设计思想,algobase.h
中还有许多类似的函数,这里就不一一分析了,下一节将会进入最为庞大的 algo.h
部分的分析。