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

Можно ли разыменовать raw pointer на тип нулевого размера, если он не указывает на выделенную память?

Можно ли разыменовать raw pointer на тип нулевого размера, если он не указывает на выделенную память?

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

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

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

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

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

Из этого следует необычное свойство: значение существует как объект Rust, хотя его размер равен нулю. Поэтому отсутствие выделенных байтов само по себе не означает, что указатель на такой объект непригоден.

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

Для обычного типа разыменование указателя означает чтение или запись занимающего память диапазона. Если диапазон не выделен, вышел за границы объекта или указатель не выровнен, возникает неопределённое поведение.

У типа нулевого размера диапазон байтов пуст, но требования к самому указателю не исчезают. Например, нулевой адрес нельзя использовать как указатель на (), а адрес, не кратный выравниванию типа, остаётся некорректным даже при нулевом размере.

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

Для разыменования raw pointer на тип нулевого размера нужно сохранить как минимум следующие условия:

  • указатель не равен null;
  • адрес соответствует выравниванию типа;
  • указатель получен или сформирован способом, допустимым моделью указателей Rust;
  • операция не создаёт ссылку с более сильными гарантиями, чем позволяет состояние объекта.

Выделение памяти для самого значения не обязательно. Стандартный способ получить ненулевой выровненный указатель-заполнитель — NonNull::dangling, но это не универсальный указатель: для ненулевого типа его разыменование было бы некорректным.

use std::ptr::NonNull; fn consume_zst() { let p: *const () = NonNull::<()>::dangling().as_ptr(); unsafe { p.read(); } }

В примере () не занимает байтов, поэтому read не извлекает данные из выделенного диапазона. Корректность обеспечивается тем, что dangling создаёт ненулевой указатель с подходящим выравниванием для ().

Это не отменяет правил арифметики указателей. Нельзя считать, что раз размер элемента равен нулю, любая арифметика или любой числовой адрес автоматически допустимы. Также нельзя переносить этот вывод на структуры, содержащие поля: структура может иметь ненулевой размер и собственные требования к валидности.

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

В обобщённой структуре хранился указатель на элемент типа T. Для T = () разработчик решил использовать нулевой указатель как признак пустого состояния, поскольку данные всё равно отсутствуют. Позже общий код начал разыменовывать указатель без отдельной проверки типа.

Рассматривались два варианта. Разрешить нулевой указатель для ZST проще, но это нарушает базовое требование валидности указателя и делает общий код некорректным. Выделять отдельный байт для каждого элемента не нужно, но это также не оправдывает использование null.

Выбранное решение — хранить ненулевой выровненный sentinel-указатель, например NonNull::<()>::dangling(), и отдельно моделировать состояние пустого контейнера. В результате код не расходует память на ZST, но сохраняет обязательные инварианты raw pointer.

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

1. Вопрос: достаточно ли того, что указатель на ZST ненулевой?

Нет. Он также должен быть корректно выровнен. Указатель с произвольным ненулевым числовым адресом может быть некорректен для типа с большим выравниванием, даже если чтение не затрагивает ни одного байта.

2. Вопрос: означает ли нулевой размер, что raw pointer можно создать из любого целого адреса?

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

3. Вопрос: можно ли применять такой же подход к T, у которого размер больше нуля?

Нет. Для ненулевого типа указатель должен охватывать доступную область памяти нужного размера, быть выровнен и указывать на корректное значение при чтении. NonNull::dangling подходит как выровненный ненулевой sentinel, но не как указатель для разыменования обычного объекта.