% Призрачные данные (PhantomData)

При работе с небезопасным кодом мы часто можем попасть в ситуацию, когда типы или времена жизни логически ассоциируются со структурой, но не являются на самом деле частью конкретного поля. Особенно часто это происходит с временами жизни. Например, Iter для &'a [T] описывается (примерно) так:

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
}

Но, в связи с тем, что 'a не используется внутри тела структуры, оно безгранично. Из-за проблем, которые исторически возникли, безграничные времена жизни и типы запрещено использовать в описании структур. Поэтому необходимо как-то перестроить эти поля внутри тела. Чтобы проверки вариантности и удаления выполнились корректно, это важно сделать правильно.

Используем PhantomData, который является специальным маркерным типом. PhantomData не занимает места в памяти, а симулирует поле необходимого типа для целей статического анализа. Считается, что так код менее подвержен ошибкам, чем при явном указании необходимой вариантности системе типов, а заодно предоставляется другая полезная информация, необходимая для проверки удаления.

Iter по логике содержит кучу &'a T, поэтому именно ее мы и будем симулировать PhantomData:

use std::marker;

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    _marker: marker::PhantomData<&'a T>,
}

И все. Время жизни будет ограничено, и ваш итератор будет вариантен над 'a и T. Все Просто Работает.

Важным примером также является Vec, который описывается (примерно) так:

struct Vec<T> {
    data: *const T, // *const для вариантности!
    len: usize,
    cap: usize,
}

В отличие от предыдущего примера кажется, что все именно так, как мы хотим. Один обобщенный аргумент в Vec проявляется по крайней мере в одном поле. Идем дальше!

Не-а.

Анализатор удалений великодушно определит, что Vec не владеет ни одним значением типа T. Это в свою очередь заставит его сделать вывод, что ему не надо волноваться при выполнении проверки правильности удаления любых T в деструкторе Vec. Что в свою очередь позволит людям создавать дыры в системе типов с помощью деструктора Vec.

Для того, чтобы объяснить анализатору удалений, что мы на самом деле владеем значениями типа T, и, следовательно, можем удалять некоторые T во время удаления всего Vec, мы должны добавить дополнительное поле PhantomData, явно утверждая:

use std::marker;

struct Vec<T> {
    data: *const T, // *const для ковариантности!
    len: usize,
    cap: usize,
    _marker: marker::PhantomData<T>,
}

Сырые указатели, владеющие пространством в памяти - это настолько повсеместная картина, что в стандартной библиотеке создали утилиту для этого, называемую Unique<T>, которая:

  • оборачивает *const T для вариантности
  • включает в себя PhantomData<T>,
  • автоматически выводит Send/Sync, как будто T их реализует,
  • помечает указатель NonZero для оптимизации нулевого указателя.