最近在看《STL 源码剖析》,恰好看到copy
函数这里。书上写的这个函数比较复杂,因为涉及到比较多提高效率的方法。因此我这篇博客就当做个简单的记录吧。
copy
函数的原型如下:
template<class InputIterator, class OutputIterator>
inline OutputIterator copy(
InputIterator first,
InputIterator last,
OutputIterator result
);
参数:
first, last
指出被复制的元素的区间范围[first,last)
result
指出复制到的目标区间起始位置
返回值:
返回一个迭代器,指出已被复制元素区间的最后一个位置
为啥copy
函数复杂,就是因为涉及到复制操作。默认的复制方法应该是调用对象的copy assignment operator
,也就是拷贝赋值运算符(这是已经对象已经初始化的情况,如果对象未初始化,调用的是uninitialized_copy
函数,它内部是使用copy constructor
,也就是拷贝构造函数来实现)。但是对于字符串,或者C++内建类型来说,直接使用memmove
拷贝底层内存的内容是更快的实现方法。因此这里就存在可以提高效率的地方。所以这个函数涉及到函数重载、type_traits、偏特化等技巧。以下系统介绍几个可以提高效率的地方:
memmove
复制底层内存来加速。如果指针指向的类型拥有trivial operator=
(例如int类型),那么直接使用memmove
复制底层内存是一种更快的方法。memmove
。但是迭代器有分InputIterator
和RandomAccessIterator
,RandomAccessIterato
r可以加速复制过程。先上一个书上的图,把copy
函数的调用流程说得明明白白的:
最外层提供三个版本的函数作为调用接口:
对应序列是字符串的情况,使用memmove
直接复制底层内存的内容:
// 同样是最上层接口,但是是重载版本,主要应对迭代器为const char*
// 这时序列为字符串,可以考虑直接使用memmove加速复制
template<>
char* copy(const char* first, const char* last, char* result) {
memmove(result, first, last - first);
return result + (last - first);
}
// 同样是最上层接口,但是是重载版本,主要应对迭代器为const wchar_t*
// 这时序列为字符串,可以考虑直接使用memmove加速复制
template<>
wchar_t* copy(const wchar_t* first, const wchar_t* last, wchar_t* result) {
memmove(result, first, sizeof(wchar_t)*(last - first));
return result + (last - first);
}
对应一般情况:
// 最上层接口,泛化版本
template <class InputIterator, class OutputIterator>
OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result) {
return __copy_dispatch<InputIterator, OutputIterator>()(first, last, result);
}
泛化版本会借助一个__copy_dispatch
结构体来应对不同的迭代器类型,从而调用不同版本的底层实现函数。__copy_dispatch
结构体本质上还是一个模板结构体,通过传入的模板参数获取迭代器的类型,并调用相应版本的函数。此外,__copy_dispatch
结构体还是一个函数对象,重载了operator()
运算符,所以可以像调用函数一样使用__copy_dispatch
结构体。
至于这里为啥要使用结构体来分发函数,而不是使用函数来分发函数,主要是因为结构体支持偏特化,而函数不支持偏特化,它只支持全特化。关于偏特化和全特化的区别,可以看这篇博客:https://harttle.land/2015/10/03/cpp-template.html。因为迭代器就算是指针,指针所指向的类型也是不确定的,而全特化不能拥有不确定的类型,它不仅需要确定迭代器类型是指针,而且还要确定指针所指向的类型,所以使用全特化不能够涵盖迭代器类型为指针的所有情况。而偏特化只是在模板的基础上进一步限定了模板参数的类型,但是它仍然可以拥有不确定的类型,所以只能用结构体偏特化。就像第一点中提到的对外接口,因为已经明确了参数必须是指向char
或者wchar_t
的指针,不存在不确定的类型,所以可以直接使用全特化版本。
// 负责copy函数分发的结构体,主要就是应对真的迭代器
// 迭代器指向的元素一般在内存上不是连续的,所以没有机会像指针迭代器那样,使用memmove复制底层元素
template <class InputIterator, class OutputIterator>
struct __copy_dispatch {
// 使结构体变为函数对象,函数参数为作为函数对象时传入的函数参数
OutputIterator operator() (InputIterator first, InputIterator last, OutputIterator result) {
typedef typename std::iterator_traits<InputIterator>::iterator_category iterator_category;
// 根据迭代器类型调用指定版本的底层copy函数
// 这里主要是如果迭代器类型为RandomAccessIterator时,可以对复制过程进行加速
// 所以需要区分不同迭代器类型的实现版本
__copy(first, last, result, iterator_category());
}
};
// 偏特化的copy函数分发的结构体,主要应对迭代器为指针类型
template <class T>
struct __copy_dispatch<T*, T*> {
// 使结构体变为函数对象,函数参数为作为函数对象时传入的函数参数
T* operator() (T* first, T* last, T* result) {
typedef typename __type_traits<T>::has_trivial_assignment_operator t;
// 根据指针指向的类型是否有trivial_assignment_operator来决定是否使用memmove加速复制
__copy_t(first, last, result, t());
}
};
// 偏特化的copy函数分发的结构体,主要应对迭代器为const指针类型
template <class T>
struct __copy_dispatch<const T*, T*> {
// 使结构体变为函数对象,函数参数为作为函数对象时传入的函数参数
T* operator() (T* first, T* last, T* result) {
typedef typename __type_traits<T>::has_trivial_assignment_operator t;
// 根据指针指向的类型是否有trivial_assignment_operator来决定是否使用memmove加速复制
__copy_t(first, last, result, t());
}
};
迭代器为指针类型,可能可以使用memmove
对底层内存内容进行复制,判断的标准是指针所指向的类型是否拥有trivial copy assignment operator
,像内建类型(int, char等)就拥有trivial copy assignment operator
,它们直接使用memmove
对底层内存内容进行复制;而拥有non-trivial copy assignment operator
的,就必须调用这个运算符来进行复制。
// 指针指向的类型有trivial_assignment_operator,也就是可以使用memmove加速复制过程
// 而不需要调用拷贝赋值运算符
template <class T>
T* __copy_t(const T* first,const T* last, T* result, __true_type) {
memmove(result, first, sizeof(T)*(last - first));
return result + (last - first);
}
// 指针指向的类型没有trivial_assignment_operator,只能使用拷贝赋值运算符
template <class T>
T* __copy_t(const T* first,const T* last, T* result, __false_type) {
// 指针本身也是RandomAccessIterator,所以也可以调用RandomAccessIterator类型迭代器对应的复制函数
__copy_d(first, last, result);
}
对应迭代器类型为常规的5种迭代器类型的版本。但是这里还需要细分成迭代器类型为InputIterator
和RandomAccessIterator
,RandomAccessIterator
直接迭代器的随机存取,因此可以加速复制过程。
// InputIterator类型迭代器调用的版本,最低效的复制方法
template <class InputIterator, class OutputIterator>
InputIterator __copy(InputIterator first, InputIterator last, OutputIterator result, std::input_iterator_tag) {
for(; first != last; ++first, ++result) {
*result = *first;
}
return result;
}
// RandomAccessIterator类型迭代器调用的版本,可以对复制过程进行优化
template <class RandomAccessIterator, class OutputIterator>
OutputIterator __copy(RandomAccessIterator first, RandomAccessIterator last,
OutputIterator result, std::random_access_iterator_tag) {
__copy_d(first, last, result);
}
这个函数其实可有可无,只是因为在RandomAccessIterator
类型迭代器调用的版本和针指向的类型没有trivial_assignment_operator
调用的版本中存在重复的操作,因此将这两个版本中的重复操作封装成一个函数,也就是__copy_d
:
// 只要迭代器类型为RandomAccessIterator(包括指针),底层都是使用这个函数进行复制
// 因为指针和RandomAccessIterator存在共同的复制操作
// 所以将这个共同的复制操作抽出来,合成一个函数
template <class RandomAccessIterator, class OutputIterator>
OutputIterator __copy_d(RandomAccessIterator first, RandomAccessIterator last, OutputIterator result) {
typedef typename std::iterator_traits<RandomAccessIterator>::difference_type Distance;
for (Distance n = last - first; n > 0; --n, ++first, ++result) {
*result = *first;
}
return result;
}
使用copy()时,如果输入区间和输出区间完全没有重叠,当然毫无问题,否则需特别注意。copy算法是一一进行元素的赋值操作,如果输出区间的起点位于输入区间内,copy算法便(可能)会在输入区间的(某些)元素尚未被复制之前,就覆盖其值,导致错误结果。如果copy算法根据其所接收的迭代器的特性决定调用memmove()来执行任务,就不会造成上述错误,因为memmove()会先将整个区间的内容复制下来,没有被覆盖的危险。
参考资料:《STL 源码剖析》