Сценарий: при сдвиге элементов буфера исходный диапазон частично перекрывается с целевым. Какую семантику копирования должен выбрать Rust-код через raw pointers?
Нужно выбрать перекрывающееся копирование — std::ptr::copy, семантически аналогичное memmove. std::ptr::copy_nonoverlapping здесь применять нельзя: при пересечении исходного и целевого диапазонов это нарушает его предусловие и приводит к неопределённому поведению.
Низкоуровневые операции копирования традиционно разделяются на две семантики: memcpy предполагает отсутствие пересечения диапазонов, а memmove корректно обрабатывает перекрытие. Rust сохраняет это различие, чтобы дать системному коду возможность явно выбрать нужные гарантии и не платить за поддержку перекрытия там, где она не нужна.
Такие операции нужны внутри реализаций контейнеров, аллокаторов, сериализаторов и обработчиков буферов. Безопасные методы срезов обычно предпочтительнее, но raw pointers позволяют управлять памятью там, где стандартных абстракций недостаточно.
При сдвиге элементов внутри одного буфера часть данных может одновременно быть источником и уже перезаписываться как часть назначения. Если копирование выполняется как для непересекающихся диапазонов, результат зависит от порядка записи: ещё не скопированные байты могут быть уничтожены.
Для copy_nonoverlapping это не просто логическая ошибка, а нарушение контракта unsafe-операции. Возможное последствие — неопределённое поведение, поэтому нельзя рассчитывать на конкретный результат, даже если на тестовой сборке сдвиг иногда выглядит правильным.
ptr::copy допускает пересечение диапазонов и выбирает корректную стратегию копирования, аналогичную memmove. При этом он не проверяет безопасность автоматически: вызывающий код обязан доказать, что исходный диапазон доступен для чтения, целевой — для записи, указатели корректно выровнены для T, а вычисленный размер не выходит за границы соответствующего объекта.
Минимальный пример сдвига байтов влево:
Здесь исходный диапазон начинается внутри buf, а целевой — с его начала, поэтому диапазоны пересекаются. Для независимых диапазонов лучше применять copy_nonoverlapping: он точнее выражает инвариант и может быть эффективнее, но требует доказательства отсутствия пересечения.
Обе операции копируют представление значений и не вызывают Drop. Для типов с владением ресурсами это критично: простое копирование байтов может временно создать несколько одинаковых представлений одного ресурса. Код должен отдельно поддержать инвариант инициализации, длины контейнера и единственного последующего уничтожения каждого значения.
Если задачу можно выразить безопасной операцией над срезом, например корректным rotate или перемещением элементов средствами контейнера, такой вариант предпочтительнее. Он переносит проверку диапазонов, инициализации и алиасинга из unsafe-кода в проверенную библиотечную абстракцию.
В собственной реализации компактного буфера парсер удаляет уже обработанный префикс и сдвигает оставшиеся байты к началу массива. Рассматривались три варианта: copy_nonoverlapping, временный буфер и ptr::copy.
copy_nonoverlapping был отвергнут из-за пересечения диапазонов. Временный буфер был проще концептуально, но добавлял выделение памяти и дополнительное копирование. Выбран ptr::copy, поскольку диапазоны находятся в одной корректно выделенной области, их размеры вычисляются до unsafe-блока, а тип элементов — u8, не имеющий проблем владения и уничтожения.
После сдвига логическая длина буфера обновляется отдельно. Это важно: само копирование байтов не сообщает контейнеру, сколько элементов теперь считается инициализированными и принадлежащими ему.
Почему нельзя просто скопировать перекрывающиеся диапазоны в обратном направлении?
Ручной выбор направления может быть корректным для конкретной операции, но он усложняет доказательство и легко ломается при изменении направления сдвига или типа данных. ptr::copy инкапсулирует именно семантику корректного перемещения при перекрытии. Если направление и границы полностью контролируются, ручной алгоритм возможен, но все инварианты всё равно остаются обязанностью unsafe-кода.
Чем опасен ptr::copy для типа с Drop?
Копирование не вызывает конструктор, перемещение или деструктор Rust. После побайтного копирования могут существовать несколько мест, содержащих одинаковое значение с одним ресурсом, например файловым дескриптором; последующее уничтожение обоих мест может привести к двойному освобождению. Нужно корректно управлять инициализированностью, длиной и владением, а для сложных типов обычно следует использовать безопасные операции контейнера.
Достаточно ли того, что исходный и целевой диапазоны находятся внутри одного среза?
Нет. Нужно также доказать корректность арифметики указателей, отсутствие выхода за объект, доступность исходных элементов для чтения, целевого диапазона для записи и соответствие выравнивания типу. Кроме того, нельзя использовать ptr::copy для обхода правил алиасинга: наличие raw pointers само по себе не разрешает одновременно несовместимые активные доступы к значению.