Подтвердить что ты не робот

Что такое "толстый указатель" в Rust?

Я уже читал термин "толстый указатель" в нескольких контекстах, но я не уверен, что именно он означает и когда он используется в Rust. Указатель кажется вдвое больше обычного, но я не понимаю, почему. Похоже, что это как-то связано с объектами черт.

4b9b3361

Ответ 1

Термин "толстый указатель" используется для обозначения ссылок и необработанных указателей на типы динамического размера (DST) - фрагменты или объекты признаков. Толстый указатель содержит указатель плюс некоторую информацию, которая делает DST "завершенным" (например, длина).

Наиболее часто используемые типы в Rust не являются DST, но имеют фиксированный размер, известный во время компиляции. Эти типы реализуют черту Sized. Даже типы, которые управляют динамическим размером динамического буфера (например, Vec<T>), являются Sized, поскольку компилятор знает точное число байтов, которое экземпляр Vec<T> займет в стеке. В настоящее время в Rust существует четыре различных типа DST.


Ломтики ([T] и str)

Тип [T] (для любого T) имеет динамический размер (как и специальный тип "среза строки" str). Вот почему вы обычно видите его только как &[T] или &mut [T], то есть за ссылкой. Эта ссылка является так называемым "указателем жира". Позвольте проверить:

dbg!(size_of::<&u32>());
dbg!(size_of::<&[u32; 2]>());
dbg!(size_of::<&[u32]>());

Это печатает (с некоторой очисткой):

size_of::<&u32>()      = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>()    = 16

Итак, мы видим, что ссылка на нормальный тип, такой как u32, имеет размер 8 байт, как и ссылка на массив [u32; 2]. Эти два типа не являются DST. Но так как [u32] является DST, ссылка на него в два раза больше. В случае срезов дополнительными данными, которые "заканчивают" DST, является просто длина. Можно сказать, что представление &[u32] выглядит примерно так:

struct SliceRef { 
    ptr: *const u32, 
    len: usize,
}


Объекты черты (dyn Trait)

При использовании признаков в качестве объектов признаков (то есть тип стирается, динамически отправляется), эти объекты признаков являются DST. Пример:

trait Animal {
    fn speak(&self);
}

struct Cat;
impl Animal for Cat {
    fn speak(&self) {
        println!("meow");
    }
}

dbg!(size_of::<&Cat>());
dbg!(size_of::<&dyn Animal>());

Это печатает (с некоторой очисткой):

size_of::<&Cat>()        = 8
size_of::<&dyn Animal>() = 16

Опять же, &Cat имеет размер всего 8 байт, потому что Cat является нормальным типом. Но dyn Animal является объектом-чертой и поэтому имеет динамический размер. Таким образом, &dyn Animal имеет размер 16 байт.

В случае объектов признаков дополнительные данные, которые завершают DST, являются указателем на vtable (vptr). Я не могу полностью объяснить концепцию vtables и vptrs здесь, но они используются для вызова правильной реализации метода в этом контексте виртуальной отправки. Vtable - это статический фрагмент данных, который в основном содержит только указатель на функцию для каждого метода. При этом ссылка на объект черты в основном представляется как:

struct TraitObjectRef {
    data_ptr: *const (),
    vptr: *const (),
}

(Это отличается от C++, где vptr для абстрактных классов хранится в объекте. Оба подхода имеют свои преимущества и недостатки.)


Пользовательские DSTs

На самом деле можно создать свои собственные DST, имея структуру, где последнее поле является DST. Это довольно редко, хотя. Одним из ярких примеров является std::path::Path.

Ссылка или указатель на пользовательский DST также является толстым указателем. Дополнительные данные зависят от типа DST внутри структуры.


Исключение: внешние типы

В RFC 1861 была введена функция extern type. Внешние типы также являются DST, но указатели на них не являются жирными указателями. Точнее, как говорит RFC:

В Rust указатели на DST содержат метаданные об объекте, на который указывает. Для строк и слайсов это длина буфера, для объектов признаков это объект vtable. Для внешних типов метаданные просто (). Это означает, что указатель на внешний тип имеет тот же размер, что и usize (т.е. это не "толстый указатель").

Но если вы не взаимодействуете с интерфейсом C, вам, вероятно, никогда не придется иметь дело с этими внешними типами.




Выше мы видели размеры неизменяемых ссылок. Жирные указатели работают одинаково для изменяемых ссылок, неизменяемых необработанных указателей и изменяемых необработанных указателей:

size_of::<&[u32]>()       = 16
size_of::<&mut [u32]>()   = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>()   = 16