Сопоставьте судьбу raw pointer на поле при перемещении значения в стек и при перемещении владельца объекта в куче.
При перемещении самого значения в стеке raw pointer на его поле больше нельзя безопасно разыменовывать: исходное место перестаёт быть местом хранения этого значения, даже если оптимизация физически не выполнила копирование. При перемещении владельца объекта в куче, например Box<T>, перемещается только указатель-владелец, а объект остаётся по прежнему адресу; raw pointer на его поле может оставаться действительным, пока объект жив и не перемещён внутри выделения.
Модель владения Rust допускает перемещение значений без скрытого копирования и без сборщика мусора. Это позволяет предсказуемо управлять ресурсами, но означает, что адрес локального значения нельзя считать стабильным после операции перемещения.
Raw pointers нужны для низкоуровневого кода, взаимодействия с ABI и внешними библиотеками. Они не владеют памятью и не продлевают её жизнь, поэтому программист сам обязан учитывать перемещения, время жизни и действительность места хранения.
Указатель на поле локальной структуры указывает не просто на набор байтов, а на конкретное место хранения. После перемещения структуры исходное место больше не содержит исходное значение в требуемом для разыменования состоянии. Разыменование такого указателя может привести к неопределённому поведению, даже если адрес выглядит доступным и память всё ещё физически выделена.
Иная ситуация возникает с Box<T>. Перемещение Box<T> обычно меняет положение самого владельца, но не положение выделенного объекта T. Поэтому указатель внутрь T не теряет действительность только из-за перемещения Box между переменными.
Нужно различать перемещение значения и перемещение владельца выделения. В первом случае меняется место, в котором находится значение; указатель на старое место нельзя использовать для чтения или записи этого значения. Во втором случае перемещается дескриптор владения, а адрес объекта в куче сохраняется.
Компилятор не обязан сохранять адрес локальной переменной после её перемещения. Нельзя делать вывод о корректности raw pointer по наблюдаемому машинному коду: оптимизатор вправе устранить или изменить физическое копирование, но правила действительности указателя определяются семантикой Rust.
Минимальный пример со стабильным адресом объекта в куче:
Здесь перемещён Box, а не объект State. Указатель действителен до уничтожения owner2, при условии что объект не был заменён, освобождён или иным способом сделан недействительным.
Если требуется гарантировать неподвижность самого значения, применяют Pin и проектируют API так, чтобы значение нельзя было переместить безопасными операциями. Однако Pin не продлевает время жизни объекта автоматически и не делает произвольное использование raw pointer безопасным: остальные инварианты всё равно должен соблюдать код.
Практическое правило: указатель на поле обычной локальной структуры нельзя сохранять через перемещение этой структуры. Для стабильного адреса используют размещение в куче, например Box, либо специальную pinned-конструкцию. При этом следует учитывать, что контейнеры вроде Vec могут переместить элементы при перераспределении буфера.
Системный модуль сохраняет raw pointer на поле состояния, а затем состояние помещается в растущий Vec. Сначала указатель работает, но последующее увеличение вектора может вызвать перераспределение и переместить все элементы. Старый указатель после этого становится недействительным.
Вариант с хранением состояния непосредственно в Vec прост и эффективен по локальности памяти, но не даёт стабильных адресов элементов. Запретить рост вектора через достаточный reserve иногда помогает на практике, однако это хрупкая гарантия: любой код, изменяющий ёмкость или структуру контейнера, может нарушить предположение.
Другой вариант — хранить элементы как Box<State> в Vec<Box<State>>. Перемещение элементов вектора перемещает только Box, поэтому адрес каждого State остаётся стабильным; цена решения — отдельные выделения памяти и менее компактное размещение.
Для объекта, который должен оставаться неподвижным из-за самоссылочной структуры или callback-контракта, выбирают Pin<Box<State>> и явно документируют инварианты. Это сложнее, зато API может не допускать обычное перемещение объекта. В результате выбор определяется тем, что именно требует внешний контракт: стабильный адрес выделения, неподвижность значения или просто временная доступность указателя.
Дополнительный вопрос 1. Может ли резервирование ёмкости Vec само по себе сделать raw pointer на элемент бессрочно действительным?
Нет. Резервирование уменьшает вероятность перераспределения, но не превращает указатель в управляемую ссылку и не продлевает время жизни элемента. Любая операция, которая перемещает элемент, изменяет контейнер или уничтожает его, должна быть исключена контрактом; обычно надёжнее хранить стабильный объект отдельно, например в Box.
Дополнительный вопрос 2. Делает ли Pin<Box<T>> raw pointer на T действительным после уничтожения Pin-владельца?
Нет. Pin ограничивает перемещение T, пока объект жив и доступен через pinned-владельца, но не передаёт владение raw pointer. После освобождения Box память больше не принадлежит объекту, и разыменование указателя становится недействительным. Для корректности нужны одновременно неподвижность объекта и достаточное время его жизни.
Дополнительный вопрос 3. Почему совпадение старого и нового числового адреса не доказывает корректность raw pointer после перемещения?
Потому что корректность определяется не только числовым адресом, но и тем, какое значение и какое место хранения этот указатель представляет. Адрес может случайно совпасть, а память может уже содержать другой объект, быть неинициализированной или не соответствовать прежнему объекту по правилам действительности указателя. Наблюдение за адресом не заменяет доказательство сохранения всех инвариантов.