Why does an option around T imply a size of std::mem::size_of::<T>() * 2

    println!("{}", std::mem::size_of::<u8>()); // 1
    println!("{}", std::mem::size_of::<Option<u8>>()); // 2
    
    println!("{}", std::mem::size_of::<u16>()); // 2
    println!("{}", std::mem::size_of::<Option<u16>>()); // 4
    
    println!("{}", std::mem::size_of::<u32>()); // 4
    println!("{}", std::mem::size_of::<Option<u32>>()); // 8

Wouldn't it make more sense to create an Option type that has the first byte of the sequence a 0 or 1, and then the remaining bytes be the size of T? It seems to me like Rust got "rid" of null at the expense of doubling memory consumption. Is this wise?

The enum tag may only need one byte, but the type as a whole needs to be aligned. So the total size is not exactly size_of::<T>() * 2, but more like size_of::<T>() + align_of::<T>().

Rust gets a little smarter than that when there are "niches" available in the type. That is when you have known invalid representations in the type which may be used to represent other enum variants. For example, Option<&T> has the same size has &T -- references can never be null, so this is a niche used for None. If you're curious to know more, this was implemented here:

3 Likes

Your hypothesis is not correct

println!("{}", std::mem::size_of::<bool>()); // 1
println!("{}", std::mem::size_of::<Option<bool>>()); // 1

By definition, enum is a sum-type or tagged union plus some optimization that compiler does to it. Checkout enum layout rules for more.

4 Likes

Ooh, I didn't know anyone had produced such nice layout docs!

Yeah, I guess it's redundant in the case of a boolean type!

Another, non-niche case is when dealing with u128s which are usually aligned to 8 bytes, so it's not dissimilar to saying:

struct OptionOfU128 {
    _u128: u128, //8 byte alignment
    tag: u8 //1 byte alignment
} //Size is technically 17, but must be rounded
  //up to the nearest multiple of the largest align
  //in the struct. Largest align is 8, so we round
  //to 24.
3 Likes