No código Rust a seguir, um Box
ou Rc
contendo um u32
tem 8 bytes, enquanto um Box
ou Rc
contendo uma fatia tem 16 bytes. Eu entendo o porquê; um ponteiro inteligente apontando para uma área de memória de tamanho dinâmico (como uma fatia) incluirá o comprimento dessa área de memória, além do endereço.
fn main()
{
println!("Size of Box<u32>: {} bytes", std::mem::size_of::<Box<u32>>()); // 8
println!("Size of Box<[u32]>: {} bytes", std::mem::size_of::<Box<[u32]>>()); // 16
println!("Size of Box<str>: {} bytes", std::mem::size_of::<Box<str>>()); // 16
println!("Size of Rc<u32>: {} bytes", std::mem::size_of::<Rc<u32>>()); // 8
println!("Size of Rc<[u32]>: {} bytes", std::mem::size_of::<Rc<[u32]>>()); // 16
println!("Size of Rc<str>: {} bytes", std::mem::size_of::<Rc<str>>()); // 16
}
Minha pergunta é como o compilador do Rust sabe qual deve ser o tamanho do ponteiro e como esse comprimento adicional é realmente usado ou interpretado.
Para começar, aqui está meu melhor palpite sobre o que está acontecendo com o tamanho do ponteiro. Ambos Rc
e Box
têm as seguintes estruturas:
pub struct Box<T: ?Sized, A: Allocator = Global>(Unique<T>, A);
pub struct Rc<T: ?Sized, A: Allocator = Global> {
ptr: NonNull<RcInner<T>>,
phantom: PhantomData<RcInner<T>>,
alloc: A,
}
Presumo que, de alguma forma, o Allocator
tipo esteja dando ao Box
/ Rc
8 bytes extras de espaço para o comprimento, mas não sei como isso está realmente acontecendo. Analisei o código do alocador, mas não entendi o que a inclusão do alocador aqui realmente faz. Se eu fosse criar meu próprio tipo de ponteiro/contêiner, incluir o Allocator
tipo na minha struct seria suficiente para ter esse comportamento?
Quanto ao modo como o comprimento é usado, meu entendimento é que tanto Box<T>
e Rc<T>
podem ser coagidos de forma transparente para &T
, e &T
é um tipo primitivo que o compilador mais ou menos espera que seja um ponteiro para uma única unidade de dados, ou um ponteiro e um comprimento para uma matriz de dados, mas nesse caso, a Deref
implementação me confunde.
impl<T: ?Sized, A: Allocator> Deref for Rc<T, A> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &T {
&self.inner().value
}
}
Sinto que esperaria que isso, de alguma forma, "perdesse" a informação sobre o comprimento, já que essa informação está incluída com o Rc
e não com o RcInner
ou com o valor T
. Mas, em vez disso, o compilador magicamente sabe disso, porque Rc<[T]>
é um ponteiro, assim como &[T]
, ele sabe que Rc
também tem esse comprimento um tanto oculto após o endereço, embora Rc
nunca o inclua explicitamente. Eu sei que o comprimento não é armazenado com , [T]
como você poderia esperar, ele é armazenado na pilha com o endereço de [T]
, seja por meio de um Box
, um Rc
, ou uma referência simples.
Então essas são minhas duas perguntas: como os dados adicionais para um ponteiro gordo são definidos/estabelecidos na própria estrutura de dados e como o Rust sabe que um tipo como Rc
ou Box
deve ser não apenas um ponteiro, mas um ponteiro gordo com informações adicionais sobre o comprimento?
Aqui estão algumas perguntas semelhantes que me ajudaram a entender como fatias/ponteiros/ponteiros grossos funcionam como pano de fundo para isso: