std::copy、std::copy_backward 和重叠范围

Posted

技术标签:

【中文标题】std::copy、std::copy_backward 和重叠范围【英文标题】:std::copy, std::copy_backward and overlapping ranges 【发布时间】:2019-10-06 01:00:01 【问题描述】:

我的参考是std::copy 和std::copy_backward。

模板 OutputIt copy( InputIt first, InputIt last, OutputIt d_first );

从 first 开始复制 [first, last) 范围内的所有元素 继续最后 - 1. 如果 d_first 在范围内,则行为未定义 范围[第一个,最后一个)。在这种情况下,可以使用 std::copy_backward 而是。

模板 BidirIt2 copy_backward( BidirIt1 第一, BidirIt1 最后, BidirIt2 d_last )

将由 [first, last) 定义的范围内的元素复制到 另一个以 d_last 结尾的范围。元素被反向复制 顺序(最后一个元素首先复制),但它们的相对顺序是 保存。

如果 d_last 在 (first, last] 范围内,则行为未定义。std::copy 在这种情况下,必须使用 std::copy_backward 代替。

复制重叠范围时,std::copy 在复制时是合适的 向左(目标范围的开头在源之外 range) 而 std::copy_backward 在复制到 右(目标范围的末尾在源范围之外)。

根据以上描述,我得出以下推论:

copy 和 copy_backward 最终都将相同的源范围 [first, last) 复制到目标范围,尽管在前者的情况下,复制是从第一个到最后一个 - 1,而在后者的情况下,复制从最后一个 -1 到第一个发生。在这两种情况下,源范围中元素的相对顺序都会保留在结果目标范围中。

但是,以下两个规定背后的技术原因是什么:

1) 在复制的情况下,如果 d_first 在 [first, last) 范围内,则会导致未定义的行为(暗示将源范围复制到目标范围不成功,并且可能出现系统故障)。

2) 在 copy_backward 的情况下,如果 d_last 在范围内(第一个,最后一个),则会导致未定义的行为(暗示将源范围复制到目标范围的失败,并可能出现系统故障)。

我假设一旦我理解了上述两个陈述的含义,将 copy 替换为 copy_backward 以避免上述未定义行为场景的建议对我来说将变得显而易见。

同样,我还假设在向左复制时提到复制的适当性(我不清楚这个概念),以及在向右复制时提到 copy_backward(我也不清楚这个概念),一旦我理解了 copy 和 copy_backward 之间的上述区别,就会开始变得有意义。

一如既往地期待您的有益想法。

附录

作为后续,我编写了以下测试代码来验证 copy 和 copy_backward 的行为,以实现相同的操作。

#include <array>
#include <algorithm>
#include <cstddef>
#include <iostream>

using std::array;
using std::copy;
using std::copy_backward;
using std::size_t;
using std::cout;
using std::endl;

int main (void)

    const size_t sz = 4;

    array<int,sz>a1 = 0,1,2,3;
    array<int,sz>a2 = 0,1,2,3;

    cout << "Array1 before copy" << endl;
    cout << "==================" << endl;

    for(auto&& i : a1) //the type of i is int&
    
        cout << i << endl;
    

    copy(a1.begin(),a1.begin()+3,a1.begin()+1);

    cout << "Array1 after copy" << endl;
    cout << "=================" << endl;

    for(auto&& i : a1) //the type of i is int&
    
        cout << i << endl;
    

    cout << "Array2 before copy backward" << endl;
    cout << "===========================" << endl;

    for(auto&& i : a2) //the type of i is int&
    
        cout << i << endl;
    

    copy_backward(a2.begin(),a2.begin()+3,a2.begin()+1);

    cout << "Array2 after copy backward" << endl;
    cout << "==========================" << endl;

    for(auto&& i : a2) //the type of i is int&
    
        cout << i << endl;
    


    return (0);

以下是程序输出:

Array1 before copy
==================
0
1
2
3
Array1 after copy
=================
0
0
1
2
Array2 before copy backward
===========================
0
1
2
3
Array2 after copy backward
==========================
2
1
2
3

显然,copy 会产生预期的结果,而 copy_backward 不会,即使 d_first 在 [first, last) 范围内。此外,d_last 也在范围内(第一个,最后一个),根据文档,这应该会在 copy_backward 的情况下导致未定义的行为。

所以实际上,在copy_backward的情况下程序输出是符合文档的,而在copy的情况下则不是。

再次值得注意的是,根据文档,在这两种情况下,d_first 和 d_last 都满足了分别导致 copy 和 copy_backward 未定义行为的条件。但是,只有在 copy_backward 的情况下才会观察到未定义的行为。

【问题讨论】:

对不起,你有什么问题? @LightnessRacesinOrbit 请参考我上面的编辑 这并没有真正澄清任何事情。请提出具体问题。 @LightnessRacesinOrbit 我已经改写了这个问题,希望现在更明显。 如果您尝试将数组的第一个和第二个插槽顺序复制到该数组的第二个和第三个插槽,请花点时间想想copy 可能会在物理上做什么。跨度> 【参考方案1】:

这里没有什么深层次的东西。只需使用简单的方法对样本数据进行算法运行:按顺序复制每个元素。

假设您有一个四元素数组int a[4] = 0, 1, 2, 3,并且您想将前三个元素复制到后三个元素。理想情况下,您最终会得到0, 0, 1, 2。这(不)如何与std::copy(a, a+3, a+1) 一起使用?

第一步:复制第一个元素a[1] = a[0];数组现在是0, 0, 2, 3

第2步:复制第二个元素a[2] = a[1];数组现在是0, 0, 0, 3

第三步:复制第三个元素a[3] = a[2];数组现在是0, 0, 0, 0

结果是错误的,因为您在读取这些值之前覆盖了一些源数据(a[1]a[2])。反向复制会起作用,因为以相反的顺序,您会在覆盖之前读取值。

由于一种合理方法的结果是错误的,因此标准将行为声明为“未定义”。希望采用幼稚方法的编译器可能会,并且他们不必考虑这种情况。在这种情况下犯错是可以的。采用不同方法的编译器可能会产生不同的结果,甚至可能是“正确”的结果。那也没关系。任何对编译器来说最简单的东西都符合标准。


鉴于问题的附录:请注意这是undefined behavior。这并不意味着行为被定义为与程序员的意图相反。相反,这意味着该行为未由 C++ 标准定义。由每个编译器决定发生什么。 std::copy(a, a+3, a+1) 的结果可以是任何东西。你可能会得到0, 0, 0, 0 的幼稚结果。但是,您可能会得到0, 0, 1, 2 的预期结果。其他结果也是可能的。你不能仅仅因为你很幸运地得到了你想要的行为就断定没有未定义的行为。 有时未定义的行为会给出正确的结果。(这就是追踪与未定义行为相关的错误如此困难的原因之一。)

【讨论】:

【参考方案2】:

原因是,一般来说,将一个范围的一部分复制到同一范围的另一部分,可能需要额外的(如果只是临时的)存储,以在从左到右或从右到按顺序复制时处理重叠留在你的第二个例子中。

与 C++ 一样,为了避免强制实现采取这种极端步骤,标准只是告诉你不要这样做,说结果是未定义的。

在这种情况下,这会迫使您通过自己复制到新的记忆中来明确。

这样做甚至不需要编译器付出任何努力来警告或告诉您这一点,这也被标准视为“太专横”。

但是您认为此处未定义的行为会导致复制失败(或系统故障)的假设也是错误的。我的意思是,这很可能结果(JaMiT 很好地演示了这是如何发生的),但你不能落入期望来自具有未定义行为的程序的任何特定结果的陷阱;这就是重点。实际上,某些实现甚至可能会遇到使重叠范围副本“工作”的麻烦(尽管我不知道有任何这样做)。

【讨论】:

我的测试代码输出与 JaMiT 的观察结果不一致。它也不符合副本的文档。 @Vinod 再次阅读我的答案。你不能期待任何特定的结果。

以上是关于std::copy、std::copy_backward 和重叠范围的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 std::copy 期间使用 std::back_inserter 而不是 end()?

使用 `std::copy()` 和 `std::back_inserter()`

是否实现了 std::copy 以使用多个线程?

在结构数组上使用 c++ std::copy

空范围的 std::copy() 或 std::move() 是不是需要有效目的地?

C 中的 std::copy 等价物