Сырые указатели и UB в Rust
Здесь проверяют границу между ссылкой и указателем: что гарантирует первая, чего не обещает второй и где именно нужен unsafe.
Стержень: создать и таскать указатель можно свободно, unsafe требуется на разыменовании, и в этот момент возвращаются все обещания ссылок.
// Формулировки: «чем *const T отличается от &T?», «где нужен unsafe при работе с указателем?», «зачем MaybeUninit?»
Ссылка обещает, указатель - нет
Ссылка это набор обещаний компилятору: объект жив, адрес выровнен, значение валидно, правило XOR (исключающее или) соблюдено. Сырой указатель не обещает ничего, поэтому его можно получить откуда угодно, скопировать, сравнить и напечатать - всё это безопасные операции.
unsafe нужен ровно на разыменовании: там компилятор уже не знает, жив ли объект и не занят ли он кем-то ещё. Проверено: создание указателя из ссылки компилируется без unsafe, а вот *p требует блока и без него даёт E0133.
// Важный момент: как только из указателя сделали &mut, обещания ссылки начинают действовать. Две живые эксклюзивные ссылки на одно место - уже UB (undefined behavior), даже если получены через указатели.
let x = 5;
let p = &x as *const i32; // безопасно
let v = unsafe { *p }; // нужен unsafe- *const T / *mut T
- сырые указатели без гарантий
- E0133
- операция требует unsafe-блока
Неинициализированная память и границы
Объявить переменную и не инициализировать нельзя: даже прочитать мусор как u32 - уже UB, компилятор рассчитывает на валидные значения. Для честного состояния «здесь пока ничего нет» есть MaybeUninit: в него пишут, а потом вызывают assume_init, беря ответственность на себя. Это нужно при заполнении буферов и в FFI (foreign function interface).
get_unchecked пропускает проверку границ. Выигрыш обычно мал - предсказатель ветвлений и так почти не ошибается, а цена ошибки чужая память. Практичнее переписать цикл на итераторы: там проверок нет по построению, потому что длина известна заранее.
// Общее правило: тянуться к unsafe ради скорости стоит только после профилировщика и с бенчмарком, показывающим реальную разницу.
- MaybeUninit
- легальная обёртка для памяти без записанного значения
- get_unchecked
- доступ к элементу без проверки границ
Как отвечать: «Чем сырой указатель отличается от ссылки?»
Размером они одинаковые, разница в обещаниях. Ссылка гарантирует, что объект жив, адрес выровнен, значение валидно и соблюдено правило одной изменяемой ссылки. Сырой указатель не гарантирует ничего, поэтому его можно создать откуда угодно, скопировать и сравнить - всё это безопасные операции. unsafe нужен на разыменовании, потому что там компилятор уже не может ничего проверить. И важная тонкость: как только из указателя сделали ссылку, её обещания начинают действовать, поэтому две живые &mut на одно место это UB, даже если они получены через указатели.
Ты называешь конкретные инварианты, а не общее «указатель небезопасен», и добавляешь последний пункт - про переход обратно к ссылкам, где ошибаются чаще всего.
На чём валятся
- −Считают, что unsafe нужен уже для создания указателя.
- −Думают, что через сырые указатели правила заимствования отменяются насовсем.
- −Читают неинициализированную память как обычный тип вместо MaybeUninit.
- −Тянутся к get_unchecked до профилирования.
- −Не могут перечислить, что именно гарантирует ссылка.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Чем *const T отличается от &T?A)Сырой указатель не скопировать: он не реализует CopyB)Сырой указатель занимает больше места: он хранит длину объектаC)Ссылка работает только со значениями на стекеD)У сырого указателя нет гарантий жизни, выравнивания и не-null
показать ответ и разбор
+D)У сырого указателя нет гарантий жизни, выравнивания и не-null// разбор: Ссылка — обещание компилятору: объект жив, адрес выровнен, значение валидно, правило XOR соблюдено. Сырой указатель не обещает ничего, поэтому его и разрешено получать из чего угодно, а обращение требует unsafe. Размер у них одинаковый, у обоих есть Copy — разница только в гарантиях.
- Из сырого указателя сделали &mut на данные, на которые уже есть другая живая &mut. Что не так?A)Ничего: правила заимствования на сырые указатели не распространяютсяB)Программа упадёт при первой же записи через вторую ссылкуC)Это UB: сломана эксклюзивность, на неё опирается оптимизаторD)Компилятор поймает это и потребует убрать одну из ссылок
показать ответ и разбор
+C)Это UB: сломана эксклюзивность, на неё опирается оптимизатор// разбор: Как только из указателя сделали ссылку, начинают действовать её обещания — в том числе noalias для &mut. Две живые эксклюзивные ссылки на одно место означают, что оптимизатор вправе кэшировать значение в регистре и не перечитывать его. Симптомов может не быть годами, а потом сломается на другом уровне оптимизации.
- Зачем нужен MaybeUninit<T>?A)Чтобы отложить выделение памяти под значение до первого обращенияB)Чтобы легально работать с памятью до инициализации значенияC)Чтобы обнулить память перед записью значенияD)Чтобы хранить значение, удаляемое в произвольный момент времени
показать ответ и разбор
+B)Чтобы легально работать с памятью до инициализации значения// разбор: Просто объявить переменную и не инициализировать нельзя: даже чтение мусора как u32 — уже UB, потому что компилятор рассчитывает на валидные значения. MaybeUninit — честная обёртка «здесь пока ничего нет»: в неё пишут, а потом вызывают assume_init, беря на себя ответственность. Нужно при заполнении буферов и в FFI.
- Что даёт get_unchecked по сравнению с обычной индексацией среза?A)Пропускает проверку границ; за корректность индекса отвечает вызывающийB)Читает элемент без выравнивания, поэтому работает с packed-структурамиC)Возвращает Option вместо паники при выходе за границуD)Возвращает копию элемента, не заимствуя срез
показать ответ и разбор
+A)Пропускает проверку границ; за корректность индекса отвечает вызывающий// разбор: Обычный доступ сравнивает индекс с длиной и паникует при выходе; get_unchecked этой проверки не делает. Выигрыш заметен разве что в горячем цикле, где предсказатель ветвлений и так почти не ошибается, а цена ошибки — чтение чужой памяти. Обычно быстрее и безопаснее переписать цикл на итераторы: там проверок нет по построению.
- Чем NonNull<T> отличается от *mut T?A)NonNull владеет данными и освобождает их при уничтожении значенияB)NonNull проверяет адрес на выравнивание при каждом обращении к даннымC)NonNull обещает ненулевой адрес, поэтому Option<NonNull<T>> занимает одно словоD)NonNull безопасен для разыменования и не требует блока unsafe при чтении
показать ответ и разбор
+C)NonNull обещает ненулевой адрес, поэтому Option<NonNull<T>> занимает одно слово// разбор: Это указатель с одним дополнительным обещанием — он не равен нулю, и компилятор пользуется этим для niche-оптимизации: Option<NonNull<T>> весит столько же, сколько сам указатель. Замер это подтверждает: 8 байт, как и у Option<&u8>. Владения он не даёт, выравнивание не проверяет, разыменование по-прежнему требует unsafe — на нём построены Box и другие умные указатели.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.