Что такое "жирный указатель" (fat pointer) в Rust и чем он отличается от обычного?
Что такое fat pointer и почему он "жирный"?
Вы всё верно подозреваете: &[i32] — это не просто адрес. В Rust fat pointer (или "широкий указатель") — это структура, которая состоит из двух обычных машинных слов (usize), а не из одного. Первое слово — это указатель на данные, а второе — метаданные (обычно длина или указатель на таблицу виртуальных методов). Именно из-за того, что он занимает в два раза больше места в памяти (например, 16 байт на 64-битной системе вместо 8), его и называют "жирным".
Тип 1: Срезы (Slices) — указатель + длина
Рассмотрим ваш пример со срезом. Когда вы пишете let slice: &[i32] = &arr[1..4];, компилятор создает fat pointer. Внутри него хранится:
- Указатель: на первый элемент среза (в вашем случае на
arr[1]). - Длина (len): количество элементов (в вашем случае 3).
Без длины Rust не смог бы проверить выход за границы массива во время выполнения (runtime bounds check). Вот как это выглядит в памяти:
// Стек:
// arr: [1, 2, 3, 4, 5] (5 * 4 байт = 20 байт)
// slice: fat pointer (16 байт на x86_64)
// [ptr: 0x7fff...] -> указывает на arr[1]
// [len: 3] -> длина среза
Если бы срез был просто обычным указателем, вы бы никогда не смогли вызвать slice.len() — эта информация была бы утеряна.
Тип 2: Трейт-объекты (dyn Trait) — указатель + vtable
Второй случай — это трейт-объекты, такие как &dyn MyTrait или Box<dyn MyTrait>. Здесь метаданные — это не длина, а указатель на vtable (виртуальную таблицу методов).
Представьте, что у вас есть трейт Animal и две структуры: Dog и Cat. Обе реализуют метод speak(). Когда вы создаете Box<dyn Animal>, fat pointer хранит:
- Указатель на данные: на экземпляр
DogилиCatв куче. - Указатель на vtable: на таблицу, где записано, где в памяти лежит функция
Dog::speak()илиCat::speak().
// Пример:
trait Animal { fn speak(&self); }
struct Dog;
impl Animal for Dog { fn speak(&self) { println!("Гав!"); } }
fn main() {
let animal: Box<dyn Animal> = Box::new(Dog);
// fat pointer (16 байт):
// [data_ptr: 0x...] -> Dog в куче
// [vtable_ptr: 0x...] -> таблица методов для Dog
animal.speak(); // Работает благодаря vtable
}
Обычный Box<Dog> — это просто указатель (8 байт), так как тип конкретный и компилятор знает все методы на этапе компиляции. Но как только вы стираете тип до dyn Animal, компилятор теряет конкретную информацию, поэтому ему приходится хранить vtable рядом с указателем на данные — отсюда и "жирность".
Итог: как отличить
- Обычный указатель/ссылка:
&T,Box<T>,*const T— 1 машинное слово (8 байт). - Fat pointer:
&[T],&str,Box<dyn Trait>,&dyn Trait— 2 машинных слова (16 байт).
Проверить это можно с помощью std::mem::size_of:
println!("{}", std::mem::size_of::<&[i32]>()); // 16
println!("{}", std::mem::size_of::<&i32>()); // 8
<<Дополнение: как передать fat pointer в C-код и почему это опасно
В вашем вопросе затронута важная тема совместимости. Если вы пишете FFI (Foreign Function Interface) и передаете срез в C-функцию, вы не можете просто передать &[i32] как один аргумент. С-функция ожидает обычный указатель (8 байт), а не fat pointer (16 байт).
Правильный способ — разобрать fat pointer на две части вручную с помощью методов .as_ptr() и .len():
let slice: &[i32] = &[1, 2, 3];
let ptr = slice.as_ptr(); // *const i32
let len = slice.len(); // usize
// Теперь можно передать в C как два отдельных параметра:
// unsafe { c_function(ptr, len); }
То же самое касается трейт-объектов. Вы не можете безопасно привести Box<dyn Trait> к *mut c_void, потому что за ним последуют лишние 8 байт метаданных. Для передачи в C нужно "раздеть" трейт-объект до конкретного типа или использовать сырые указатели и vtable вручную (что крайне сложно).
Таким образом, fat pointer — это мощный инструмент Rust для безопасности типов, но он несовместим с ABI (Application Binary Interface) языка C, где указатели всегда однозначны.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги