std::copy, std::copy_向后和重叠范围

std::copy, std::copy_backward and overlapping ranges

我的参考文献是std::copy and std::copy_backward

template< class InputIt, class OutputIt > OutputIt copy( InputIt first, InputIt last, OutputIt d_first );

Copies all elements in the range [first, last) starting from first and proceeding to last - 1. The behavior is undefined if d_first is within the range [first, last). In this case, std::copy_backward may be used instead.

template< class BidirIt1, class BidirIt2 > BidirIt2 copy_backward( BidirIt1 first, BidirIt1 last, BidirIt2 d_last )

Copies the elements from the range, defined by [first, last), to another range ending at d_last. The elements are copied in reverse order (the last element is copied first), but their relative order is preserved.

The behavior is undefined if d_last is within (first, last]. std::copy must be used instead of std::copy_backward in that case.

When copying overlapping ranges, std::copy is appropriate when copying to the left (beginning of the destination range is outside the source range) while std::copy_backward is appropriate when copying to the right (end of the destination range is outside the source range).

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

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

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

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

2) 在 copy_backward 的情况下,如果 d_last 在范围内(首先,最后一个].

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

同样,我也假设在向左复制时提到复制的适当性(我不清楚这个概念),而在向右复制时copy_backward(这个概念不清楚)对我来说也很清楚),一旦我理解了复制和 copy_backward.

之间的上述区别,就会开始有意义

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

附录

作为后续,我编写了以下测试代码来验证复制和 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 也在范围内 (first, last],根据文档,在 copy_backward 的情况下应该会导致未定义的行为。

所以实际上,在 copy_backward 的情况下,程序输出与文档一致,而在复制的情况下则不然。

再次值得注意的是,在这两种情况下,d_first 和 d_last 确实满足条件,根据文档,该条件应该分别导致复制和 copy_backward 的未定义行为.但是,仅在 copy_backward.

的情况下才会观察到未定义的行为

这里没有深入的内容。只需使用简单的方法对示例数据执行算法 运行:按顺序复制每个元素。

假设您有一个四元素数组 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}.

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

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

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

由于一种合理方法的结果是错误的,因此标准声明了行为 "undefined"。希望采用天真的方法的编译器可以,而且他们不必考虑这种情况。在这种情况下犯错是可以的。采用不同方法的编译器可能会产生不同的结果,甚至可能是 "correct" 结果。那也可以。按照标准,对于编译器来说最简单的就是好的。


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

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

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

在这种情况下,这会迫使您通过自己复制到新的内存中来显式显示。

它这样做甚至不需要编译器做出任何努力来警告或告诉你这件事,这在标准方面也被视为 "too bossy"。

但是您假设这里的未定义行为会导致复制失败(或系统故障)也是错误的。我的意思是,这很可能 成为 结果(并且 JaMiT 很好地演示了这是如何发生的),但您一定不要陷入期望来自具有未定义行为的程序的任何特定结果的陷阱;这就是重点。事实上,某些实现甚至可能会产生重叠范围副本的麻烦 "work"(尽管我不知道这样做)。