从零开始的 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 部分的分析。



