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

91

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

Лукас Калбертодт
источник
7
Кстати, сам термин не относится к Rust. Жирный указатель обычно относится к указателю, который хранит некоторые дополнительные данные, помимо адреса объекта, на который указывает. Если указатель содержит некоторые биты тега и, в зависимости от этих битов тега, указатель иногда вообще не является указателем, это называется представлением тегированного указателя . (Например , на многих Smalltalks виртуальных машин, указатели, заканчивающиеся 1 бит фактически 31/63-битные целые числа, так как указатели слова выровнены и , таким образом , никогда не заканчиваются в 1.) HotSpot JVM называет своего толстяка указателей ООП с (Object-Oriented Указатели).
Jörg W Mittag
1
Просто предложение: когда я публикую пару вопросов и ответов, я обычно пишу небольшую заметку, объясняющую, что это вопрос, на который я ответил сам, и почему я решил опубликовать его. Взгляните на сноску в вопросе здесь: stackoverflow.com/q/46147231/5768908
Херардо Фуртадо
@GerardoFurtado Я изначально разместил здесь комментарий, объясняющий именно это. Но это было удалено сейчас (не мной). Но да, согласен, часто такая заметка бывает полезной!
Лукас Калбертодт

Ответы:

102

Термин «толстый указатель» используется для обозначения ссылок и необработанных указателей на типы с динамическим размером (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]. Эти два типа не относятся к летнему времени. Но, как и [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 для абстрактных классов хранится внутри объекта. Оба подхода имеют свои преимущества и недостатки.)


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

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

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


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

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

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

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




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

size_of::<&[u32]>()       = 16
size_of::<&mut [u32]>()   = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>()   = 16
Лукас Калбертодт
источник