Программирование RustUnsafe и памятьРазработчик системного Rust

После операции reserve у Vec сохранённый raw pointer имеет тот же числовой адрес. Достаточно ли этого для б...

После операции reserve у Vec<T> сохранённый raw pointer имеет тот же числовой адрес. Достаточно ли этого для безопасного доступа?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет. Совпадение числового адреса не доказывает, что raw pointer всё ещё указывает на действующий объект: reserve мог перевыделить буфер, после чего старый указатель недействителен. Указатель можно использовать только если доказано, что перевыделения не было и сам элемент не был перемещён или уничтожен.

Исторический контекст

Vec<T> хранит элементы в непрерывном буфере, который может перемещаться при увеличении ёмкости. Это позволяет в обычном случае получать компактное размещение и эффективно добавлять элементы, но адреса элементов не являются стабильными на всём сроке жизни вектора.

Модель владения Rust не запрещает создавать raw pointers, потому что они нужны для низкоуровневых структур данных и FFI. Однако raw pointer не получает автоматической гарантии от владельца Vec: ответственность за срок действия указателя остаётся у unsafe-кода.

Постановка проблемы

При недостаточной ёмкости reserve может выделить новый буфер, переместить туда элементы и освободить старый. Сохранённый указатель продолжит содержать прежний адрес, но этот адрес уже не будет обозначать соответствующий объект T.

Проверка только числового равенства адресов ненадёжна. Аллокатор теоретически может вернуть тот же адрес, однако это не превращает указатель из прежнего размещения в доказанно действующий указатель на новое размещение. Использование недействительного указателя приводит к неопределённому поведению, даже если чтение внешне возвращает ожидаемое значение.

Подробное решение

Raw pointer на элемент Vec<T> действителен лишь при одновременном выполнении нескольких условий: сам Vec жив, его буфер не был перевыделен, нужный элемент всё ещё существует, а операции над вектором не изменили смысл позиции элемента.

reserve может перевыделить память, но не обязан это делать. Если ёмкости уже достаточно, адреса элементов обычно сохраняются. Тем не менее код, который должен быть корректным независимо от текущего состояния ёмкости, не должен полагаться на наблюдение адреса после операции.

Надёжный приём — завершить операции, способные изменить размещение, до получения указателя:

let mut values = vec![10, 20]; values.reserve(1); let p = values.as_ptr(); values.push(30); // зарезервированной ёмкости достаточно assert_eq!(unsafe { *p }, 10);

Здесь reserve(1) гарантирует достаточную дополнительную ёмкость для последующей вставки одного элемента, поэтому push не должен перевыделять буфер. Но такой указатель всё равно нельзя использовать после операций, которые могут вызвать новое перевыделение, удалить элемент или сдвинуть элементы.

Для долгоживущих ссылок на объекты часто выбирают другой дизайн: хранят элементы в Box<T> или другой стабильной аллокации, используют индексы вместо указателей либо заранее фиксируют границы допустимых изменений. Простое предварительное резервирование — лишь локальная гарантия для известного ограниченного сценария, а не свойство стабильности Vec.

Ситуация из практики

Unsafe-структура хранит raw pointer на элемент Vec и после этого добавляет элементы в тот же вектор. Рассматривались три варианта.

Первый — сравнивать старый указатель с новым адресом буфера. Это не даёт достаточной гарантии: числовое равенство адресов не доказывает сохранность исходного объекта и не защищает от удаления или сдвига элементов.

Второй — заранее вызвать reserve с большим запасом. Это подходит, если максимальное число последующих добавлений известно и все операции действительно сохраняют нужные элементы. Минус — расход памяти и хрупкость гарантии при изменении кода.

Третий — заменить указатель индексом и получать доступ к элементу только после завершения изменений вектора. Был выбран третий вариант: индекс не зависит от адреса буфера, а проверка границ выполняется в момент доступа. Если требуется стабильный адрес, элементы вынесли в отдельные Box<T>, а вектор стал хранить владельцев этих объектов. Такой дизайн устранил неопределённое поведение при перевыделении и сделал контракт обёртки явным.

Что кандидаты часто упускают

1. Всегда ли push делает raw pointer на элемент Vec недействительным?

Нет. push делает такой указатель недействительным, если вызывает перевыделение или если последующие операции меняют сам элемент, на который он логически ссылался. Если ёмкости достаточно и существующие элементы не перемещаются, указатель на них может оставаться действительным. Но это должно быть доказано контрактом программы, а не предположено по текущей ёмкости.

2. Сохраняет ли raw pointer позицию элемента после удаления другого элемента из Vec?

Не обязательно. Даже без перевыделения удаление может сдвинуть последующие элементы в памяти. Старый адрес иногда останется выровненным и доступным, но теперь будет относиться к другому элементу или к неинициализированной области. Поэтому указатель может потерять не только физическую валидность, но и соответствие исходному объекту.

3. Делает ли Box<T> адрес объекта стабильным при перемещении самого Box?

Да, перемещение владельца Box<T> обычно перемещает сам указатель-владелец, а объект T остаётся в куче по тому же адресу. Поэтому raw pointer на T может пережить перемещение Box, пока объект не уничтожен и не заменён. Это не означает, что такой указатель автоматически становится безопасным: нужно по-прежнему обеспечить срок жизни объекта, отсутствие конфликтующего доступа и корректность типа.