STL算法移动范围
- 一、简介
- 二、批量移动集合中的多个元素
- 2.1、std::copy
- 2.2、std::move
- 2.3、std::swap_ranges
- 三、在一个范围内变换子范围
- 3.1、std::copy_backward 向前复制
- 3.2、元素倒退
- 3.3、交换子范围
- 四、这一切太复杂了
- 五、总结
一、简介
已经介绍过使用STL算法在范围(range)上实现复杂操作的各种方法。本文将重点介绍如何在STL中移动集合中的元素,以及一些常用的移动算法的实现和用法。包括批量移动元素、子范围变换和交换元素等。
移动集合中的元素在实际应用中非常常见,对于提高程序效率和减少资源占用都具有重要意义。通过合理选择移动算法,可以更加高效地操作集合中的数据,实现更灵活的功能。
二、批量移动集合中的多个元素
基本上有三种允许批量移动集合中的多个元素的STL算法:std::copy
、std::move
和std::swap_ranges
。
2.1、std::copy
std::copy
可能是STL列表中最简单的算法。它接受一个输入范围(以两个迭代器的形式,和现在的STL接口一样)和一个输出迭代器:
template<typename InputIterator, typename OutputIterator >
OutputIterator copy(InputIterator first, InputIterator last, OutputIterator out);
它只是简单地将输入范围的每个元素复制到输出迭代器中,每一步增加一个元素。
当它的一个输入或输出没有绑定到容器时,它会变得更加微妙。例如,将输出迭代器绑定到流的情况:
std::vector<int> v = {1, 2, 3, 4, 5};
std::copy(begin(v), end(v), std::ostream_iterator<int>(std::cout));
输出:
12345
std::copy
的另一个微妙之处是,如果范围的元素类型的复制构造函数满足某些条件(更准确地说,如果它是std::is_trivially_copyable
), std::copy
可以调用std::memmove
来批量获取内存块,而不是对每个元素调用复制构造函数。
但总的来说,这不是一个非常微妙的算法。
注意,std::copy
有一个对应的"_n
":std::copy_n
。它以begin
迭代器和大小的形式接受输入范围,而不是begin
和end
:
template<typename InputIterator, typename Size, typename OutputIterator>
OutputIterator copy_n(InputIterator first, Size count, OutputIterator out);
此外,要将范围复制到STL容器中,不仅仅只有std::move
请,还有其他方法可以有效地将多个元素插入STL容器中,会有专门的文章介绍。
2.2、std::move
std::move
是C++ 11带来的最基本的标准函数之一,如果还不知道,那你需要好好的重温以下C++ 11的知识了。
如果了解std::move
,是否也知道std::move
对范围也有重载呢?和std::copy
一样,它接受两个输入迭代器和一个输出迭代器:
template<typename InputIterator, typename OutputIterator>
OutputIterator move(InputIterator first, InputIterator last, OutputIterator out);
它将输入范围的每个元素都移动到输出迭代器中:
这是一种不同于将迭代器移动来让STL移动元素的方法。
2.3、std::swap_ranges
顾名思义,std::swap_ranges
将第一个范围中的每个元素与第二个范围中的对应元素交换:
注意,这两个范围不允许重叠。
有点奇怪的是,std::swap_range
和 std::move
的名称不对称,也许使用 std::move_ranges
或者对 std::swap
重载会更一致。算了,尊重标准吧。
还要注意std::swap_ranges
是一个“1.5 range”,也就是说它不接受第二个范围的末尾:
template<typename ForwardIterator1, typename ForwardIterator2>
ForwardIterator2 swap_ranges(ForwardIterator1 first1, ForwardIterator1 last1,
ForwardIterator2 first2);
它假设第二个范围至少和第一个范围一样大,所以在调用std::swap_ranges
之前,需要确保这个假设是正确的。
三、在一个范围内变换子范围
上述三种算法可以将数据从一个范围传输到另一个范围。但如果这两个范围实际上是更大范围的两个子范围呢?如果这些子范围重叠呢?
3.1、std::copy_backward 向前复制
假如想要复制一个范围的子部分到该范围某个的位置,这个新位置可能位于第一个子范围的末尾之前。
例如,一个1到10的范围:
想将1到5的子范围向后移动3个位置:
第一反应可能是使用std::copy
:
std::copy(begin(v), begin(v) + 5, begin(v) + 3);
或者,std::copy_n
:
std::copy_n(begin(v), 5, begin(v) + 3);
但是,这不是这个操作的正确算法,原因是:
- 第一个原因是,它不会得到我们期望的结果。就像下面这样:
哦~~,第一步就失去了4
的值。 - 第二个原因是标准要求输出迭代器不在
[begin, end]
之内。因此,std::copy
实际上具有未定义行为。这有一个奇怪的含义,即禁止在自身上复制一个范围。
因此,要在一个范围内向前复制值,需要一个与std::copy
相同的算法,但是向后(这听起来有点奇怪)。
这就是为什么有…std::copy_backward
!std::copy_backward
类似于std::copy
,不同之处在于它首先将输入范围的最后一个元素复制到输出范围的最后一个元素:
然后它从这里开始工作,一直到输入范围的开始:
这意味着指向输出范围的输出迭代器必须是它的end
:
template<typename BidirectionalIterator1, typename BidirectionalIterator2>
BidirectionalIterator2 copy_backward(BidirectionalIterator1 first, BidirectionalIterator1 last,
BidirectionalIterator2 outLast);
所以,例子中的代码应该这样:
std::copy_backward(begin(v), begin(v) + 5, begin(v) + 8);
注意,还有std::move_backward
,它移动一个范围的元素,从它的末尾开始,一直移动到它的开始。
3.2、元素倒退
与上面类似的推理,要向后移动,可以使用std::copy
(或std::move
)。实际上,如果std::copy_backward
的输出迭代器位于输入范围的(begin, end)
内,则是未定义的行为。
3.3、交换子范围
可以使用std::swap_ranges
在一个范围内交换两个子范围,只要它们不重叠即可。
四、这一切太复杂了
使用std::copy_backward
向前移动元素,确保所有的开始和结束迭代器都是正确的,以避免超出范围……这一切看起来都很复杂。
因此,有学者提议在C++ 20标准中增加一个std::shift_left
和一个std::shift_right
函数。建议的函数原型为:
template<typename ForwardIterator>
ForwardIterator shift_left(ForwardIterator first, ForwardIterator last,
typename std::iterator_traits<ForwardIterator>::difference_type n);
template<class ForwardIterator>
ForwardIterator shift_right(ForwardIterator first, ForwardIterator last,
typename std::iterator_traits<ForwardIterator>::difference_type n);
最后一个参数表示移动元素的步数,因此:
std::shift_right(begin(v), begin(v) + 5, 3);
会把范围3的前5个元素的位置移到范围下面。注意:这两个函数会移动,而不会复制元素。然而,C++ 20并没有对应的函数,但是有人已经实现了该函数,并上传在github上。
五、总结
本文详细介绍了在STL中批量移动集合中的多个元素的几种常见算法,包括std::copy
、std::move
和std::swap_ranges
。通过这些算法,我们可以在不同的情况下灵活地对集合进行移动操作,实现数据的复制、移动和交换。
另外,还介绍了在一个范围内变换子范围的方法,以及如何向前复制元素、元素倒退和交换子范围。这些技巧可以帮助我们更好地处理集合中元素的移动和交换操作,提高代码的效率和可读性。
最后,虽然C++ 20标准没有引入std::shift_left
和std::shift_right
函数,但可以通过自定义实现或使用第三方库来实现这些功能,以满足特定需求和提高编程效率。