STL源码剖析——copy函数

前言

最近在看《STL 源码剖析》,恰好看到copy函数这里。书上写的这个函数比较复杂,因为涉及到比较多提高效率的方法。因此我这篇博客就当做个简单的记录吧。

copy函数的原型如下:

template<class InputIterator, class OutputIterator>  
inline OutputIterator copy(  
      InputIterator first,   
      InputIterator last,   
      OutputIterator result  
);

参数:
first, last
指出被复制的元素的区间范围[first,last)
result 
指出复制到的目标区间起始位置

返回值:
返回一个迭代器,指出已被复制元素区间的最后一个位置

copy函数可优化的地方

为啥copy函数复杂,就是因为涉及到复制操作。默认的复制方法应该是调用对象的copy assignment operator,也就是拷贝赋值运算符(这是已经对象已经初始化的情况,如果对象未初始化,调用的是uninitialized_copy函数,它内部是使用copy constructor,也就是拷贝构造函数来实现)。但是对于字符串,或者C++内建类型来说,直接使用memmove拷贝底层内存的内容是更快的实现方法。因此这里就存在可以提高效率的地方。所以这个函数涉及到函数重载、type_traits、偏特化等技巧。以下系统介绍几个可以提高效率的地方:

  1. 如果迭代器指向的序列是字符串,那么直接使用字符串拷贝,效率会提高。
  2. 如果迭代器本身是指针,那么它所指向的这个序列在内存上是连续存放的,所以可能可以使用memmove复制底层内存来加速。如果指针指向的类型拥有trivial operator=(例如int类型),那么直接使用memmove复制底层内存是一种更快的方法。
  3. 如果迭代器本身就是迭代器,那么所指向的元素在内存上可能不是连续存放的,所以不能使用memmove。但是迭代器有分InputIteratorRandomAccessIteratorRandomAccessIterator可以加速复制过程。

copy函数的具体实现

1. 算法总览

先上一个书上的图,把copy函数的调用流程说得明明白白的:

STL源码剖析——copy函数_第1张图片

2. 具体实现

最外层提供三个版本的函数作为调用接口:

2.1 外层接口:两个全特化版本

对应序列是字符串的情况,使用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);
}

2.2 外层接口:泛化版本

对应一般情况:

// 最上层接口,泛化版本
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());
    }
};

2.2.1 __copy_dispatch分发:指针类型的偏特化版本

迭代器为指针类型,可能可以使用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);
}

2.2.2 __copy_dispatch分发:普通迭代器类型的泛化版本

对应迭代器类型为常规的5种迭代器类型的版本。但是这里还需要细分成迭代器类型为InputIteratorRandomAccessIteratorRandomAccessIterator直接迭代器的随机存取,因此可以加速复制过程。

// 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);
}
2.2.2.1 最后一个底层的函数__copy_d

这个函数其实可有可无,只是因为在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算法便(可能)会在输入区间的(某些)元素尚未被复制之前,就覆盖其值,导致错误结果。如果copy算法根据其所接收的迭代器的特性决定调用memmove()来执行任务,就不会造成上述错误,因为memmove()会先将整个区间的内容复制下来,没有被覆盖的危险。

STL源码剖析——copy函数_第2张图片


参考资料:《STL 源码剖析》

你可能感兴趣的:(C++)