std :: copy, std :: copy_backward и перекрывающиеся диапазоны - PullRequest
2 голосов
/ 06 октября 2019

Мои ссылки на std :: copy и std :: copy_backward .

template <класс InputIt, класс OutputIt> OutputIt copy (InputItfirst, InputIt last, OutputIt d_first);

Копирует все элементы в диапазоне [first, last) начиная с первого и заканчивая последним - 1. Поведение не определено, если d_first находится в диапазоне [first, last). В этом случае вместо этого можно использовать std :: copy_backward.

template <класс BidirIt1, класс BidirIt2> BidirIt2 copy_backward (сначала BidirIt1, BidirIt1, Last, BidirIt2 d_last)

Копирует элементы издиапазон, определенный [first, last), другому диапазону, оканчивающемуся на d_last. Элементы копируются в обратном порядке (последний элемент копируется первым), но их относительный порядок сохраняется.

Поведение не определено, если d_last находится внутри (first, last]. Необходимо использовать std :: copyвместо std :: copy_backward в этом случае.

При копировании перекрывающихся диапазонов, std :: copy подходит для копирования влево (начало целевого диапазона находится за пределами исходного диапазона), а std :: copy_backward -уместно при копировании вправо (конец целевого диапазона находится за пределами исходного диапазона).

Из приведенного выше описания я получаю следующий вывод:

И copy, и copy_backward endдо того же самого исходного диапазона [first, last) до целевого диапазона, хотя в первом случае копирование происходит с первого до последнего - 1, тогда как в случае последнего копирование происходит с последнего -1 до первого. В обоих случаях относительный порядок элементов в исходном диапазоне сохраняется в результирующем целевом диапазоне.

Однако, какова техническая причина, лежащая в основе следующих двух условий:

1) В случаекопирования, неопределенные результаты поведения (подразумевающие неудачное копирование исходного диапазона в целевой диапазон и, возможно, системную ошибку), если d_first находится в пределах диапазона [first, last).

2) В случае copy_backward, undefinedрезультаты поведения (подразумевающие неудачное копирование исходного диапазона в целевой диапазон и, возможно, системную ошибку), если d_last находится в пределах диапазона (первый, последний).

Я предполагаю, что предложение заменить copy на copy_backward для avertописанный выше неопределенный сценарий поведения станет для меня очевидным, как только я пойму смысл двух приведенных выше утверждений.

Аналогично, я также предполагаю, что упоминание о целесообразности копирования при копировании влево (какое понятиене понятно мe) и 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_backward - нет, даже если d_first находится в диапазоне [first, last). Кроме того, d_last также находится в пределах диапазона (first, last), что должно привести к неопределенному поведению в случае copy_backward в соответствии с документацией.

Таким образом, выходные данные программы соответствуют документациив случае copy_backward, тогда как в случае копирования это не так.

Стоит еще раз отметить, что в обоих случаях d_first и d_last удовлетворяют условию, которое должно привести к неопределенному поведению как для copy, так и для copy_backward. соответственно, согласно документации. Однако неопределенное поведение наблюдается только в случае copy_backward.

Ответы [ 2 ]

2 голосов
/ 06 октября 2019

Здесь глубоко ничего не происходит. Просто выполните прогон алгоритма с примерами данных, используя наивный подход: скопируйте каждый элемент по порядку.

Предположим, у вас есть массив из четырех элементов int a[4] = {0, 1, 2, 3}, и вы хотите скопировать первые три элемента в последнийтри. В идеале вы должны получить {0, 0, 1, 2}. Как это (не) будет работать с std::copy(a, a+3, a+1)?

Шаг 1: Скопировать первый элемент a[1] = a[0]; Массив теперь {0, 0, 2, 3}.

Шаг 2: Скопируйте второй элемент a[2] = a[1]; Массив теперь {0, 0, 0, 3}.

Шаг 3: Скопируйте третий элемент a[3] = a[2]; Массив теперь {0, 0, 0, 0}.

Результат неверный, поскольку вы перезаписали некоторые исходные данные (a[1] и a[2]) перед чтением этих значений. Копирование в обратном порядке будет работать, потому что в обратном порядке вы должны прочитать значения перед тем, как их перезаписать.

Поскольку при одном разумном подходе результат неверен, стандарт объявил поведение «неопределенным». Составители, желающие принять наивный подход, могут, и они не должны учитывать этот случай. Это нормально, чтобы быть неправым в этом случае. Компиляторы, использующие другой подход, могут давать разные результаты, возможно, даже «правильные» результаты. Это тоже нормально. Все, что проще для компилятора, прекрасно по стандарту.


В свете добавления к вопросу: обратите внимание, что это неопределенное поведение . Это не означает, что поведение определено как противоречащее замыслу программиста. Скорее это означает, что поведение не определено стандартом C ++. Каждый компилятор должен решить, что произойдет. Результат std::copy(a, a+3, a+1) может быть любым. Вы можете получить наивный результат {0, 0, 0, 0}. Однако вместо этого вы можете получить ожидаемый результат {0, 0, 1, 2}. Другие результаты также возможны. Вы не можете сделать вывод, что не существует неопределенного поведения просто потому, что вам повезло получить то поведение, которое вы хотели. Иногда неопределенное поведение дает правильные результаты. (Это одна из причин, по которой отслеживание ошибок, связанных с неопределенным поведением, может быть настолько сложным.)

1 голос
/ 06 октября 2019

Причина в том, что, как правило, копирование части диапазона в другую часть этого же диапазона может потребовать дополнительного (хотя бы временного) хранения для обработки перекрытий при последовательном копировании слева направо или справа налевооставлен во втором примере.

Как и в случае с C ++, чтобы избежать принуждения реализаций к такому крайнему шагу, стандарт просто запрещает делать это, говоря, что результаты не определены.

Это заставляет вас, в таких ситуациях, быть явным, копируя в новый фрагмент памяти самостоятельно.

Это делает это, даже не требуя от компилятора каких-либо усилий предупреждать или сообщать вам об этом, чтотакже следует рассматривать как «слишком властный» со стороны стандарта.

Но ваше предположение, что неопределенное поведение здесь приводит к ошибке копирования (или системной ошибке), также неверно. Я имею в виду, что вполне может быть результатом (и JaMiT очень хорошо демонстрирует, как это может происходить), но вы не должны попадать в ловушку ожидания какого-либо конкретного результата от программы с неопределенным поведением;в этом суть. На самом деле, некоторые реализации могут даже заставить работать "перекрывающиеся" копии диапазона (хотя я не знаю ни одного из них).

...