сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · unsafe и производительность Rust

Сырые указатели и 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, остальные разбираются в тренажёре.

  1. #rs_raw_ptr1 / 5
    Чем *const T отличается от &T?
    A)Сырой указатель не скопировать: он не реализует Copy
    B)Сырой указатель занимает больше места: он хранит длину объекта
    C)Ссылка работает только со значениями на стеке
    D)У сырого указателя нет гарантий жизни, выравнивания и не-null
    показать ответ и разбор
    +D)У сырого указателя нет гарантий жизни, выравнивания и не-null

    // разбор: Ссылка — обещание компилятору: объект жив, адрес выровнен, значение валидно, правило XOR соблюдено. Сырой указатель не обещает ничего, поэтому его и разрешено получать из чего угодно, а обращение требует unsafe. Размер у них одинаковый, у обоих есть Copy — разница только в гарантиях.

  2. #rs_raw_ptr2 / 5
    Из сырого указателя сделали &mut на данные, на которые уже есть другая живая &mut. Что не так?
    A)Ничего: правила заимствования на сырые указатели не распространяются
    B)Программа упадёт при первой же записи через вторую ссылку
    C)Это UB: сломана эксклюзивность, на неё опирается оптимизатор
    D)Компилятор поймает это и потребует убрать одну из ссылок
    показать ответ и разбор
    +C)Это UB: сломана эксклюзивность, на неё опирается оптимизатор

    // разбор: Как только из указателя сделали ссылку, начинают действовать её обещания — в том числе noalias для &mut. Две живые эксклюзивные ссылки на одно место означают, что оптимизатор вправе кэшировать значение в регистре и не перечитывать его. Симптомов может не быть годами, а потом сломается на другом уровне оптимизации.

  3. #rs_raw_ptr3 / 5
    Зачем нужен MaybeUninit<T>?
    A)Чтобы отложить выделение памяти под значение до первого обращения
    B)Чтобы легально работать с памятью до инициализации значения
    C)Чтобы обнулить память перед записью значения
    D)Чтобы хранить значение, удаляемое в произвольный момент времени
    показать ответ и разбор
    +B)Чтобы легально работать с памятью до инициализации значения

    // разбор: Просто объявить переменную и не инициализировать нельзя: даже чтение мусора как u32 — уже UB, потому что компилятор рассчитывает на валидные значения. MaybeUninit — честная обёртка «здесь пока ничего нет»: в неё пишут, а потом вызывают assume_init, беря на себя ответственность. Нужно при заполнении буферов и в FFI.

  4. #rs_raw_ptr4 / 5
    Что даёт get_unchecked по сравнению с обычной индексацией среза?
    A)Пропускает проверку границ; за корректность индекса отвечает вызывающий
    B)Читает элемент без выравнивания, поэтому работает с packed-структурами
    C)Возвращает Option вместо паники при выходе за границу
    D)Возвращает копию элемента, не заимствуя срез
    показать ответ и разбор
    +A)Пропускает проверку границ; за корректность индекса отвечает вызывающий

    // разбор: Обычный доступ сравнивает индекс с длиной и паникует при выходе; get_unchecked этой проверки не делает. Выигрыш заметен разве что в горячем цикле, где предсказатель ветвлений и так почти не ошибается, а цена ошибки — чтение чужой памяти. Обычно быстрее и безопаснее переписать цикл на итераторы: там проверок нет по построению.

  5. #rs_raw_ptr5 / 5
    Чем 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 и другие умные указатели.

дальше

Теорию прочитали. Навык ставится повторением

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