Question regarding enum discriminant elision

Hey folks,

consider this code:

use std::mem;
use std::num::NonZero;

fn main() {
    let a: Option<NonZero<u32>> = NonZero::<u32>::new(1);
    let b: Option<Option<NonZero<u32>>> = Some(NonZero::<u32>::new(1));
    let c: Option<Option<Option<NonZero<u32>>>> = Some(Some(NonZero::<u32>::new(1)));
    let d: Option<Option<Option<Option<NonZero<u32>>>>> = Some(Some(Some(NonZero::<u32>::new(1))));
    println!("{}", mem::size_of_val(&a)); // 4 (Bytes)
    println!("{}", mem::size_of_val(&b)); // 8 (Bytes)
    println!("{}", mem::size_of_val(&c)); // 8 (Bytes)
    println!("{}", mem::size_of_val(&d)); // 8 (Bytes)
}

I wonder why b requires double the size in memory as a. Wouldn't it be ok to just refer to the most inner discriminant instead of requiring 4 more bytes for another discriminant?

Regards
keks

There's u32::MAX + 1 different states Option<Option<NonZero<u32>>> can be:

  • Some(Some(1 to u32::MAX))
  • Some(None)
  • None

This would fit in 5 bytes, but it gets rounded up to 8 to satisfy alignment.

6 Likes

NonZero gives enough room for None by repurposing the 0 value, but if you wrap another Option around it, where else is there to put another None? So expanding the type to use more space is only natural.

1 Like

Ahhh you're right. :slight_smile: When playing around I had for some reason the idea that the most inner discriminant could represent here "all Nones" which is of course wrong. :exploding_head:

Thank you for your quick answers and explainations! :folded_hands:

Start from a smaller type and you'll see more layers of Option are possible without the value getting bigger. For example, try Option<Option<Option<Option<Option<Option<bool>>>>>>.

1 Like

That's actually u32::MAX + 2 different states:

  • Some(Some(_)) -- u32::MAX different states (232 - 1)
  • Some(None) -- 1 state
  • None -- 1 state
1 Like