# Unexpected size of union

**URL:** <https://ziggit.dev/t/unexpected-size-of-union/17018>\
**Category:** Explain\
**Created:** [July 31, 2026, 6:33pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018 "2026-07-31T18:33:49Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![giluis](https://ziggit.dev/user_avatar/ziggit.dev/giluis/32/5886_2.png) [@giluis](https://ziggit.dev/u/giluis)\
**Post date:** [July 31, 2026, 6:33pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/1 "2026-07-31T18:33:49Z")

</div>

Hey!

The following code prints `2`. I don’t understand why that happens.  
I expected it to print `1`, because the payloads are just 6 bits, meaning the discriminant / tag could be stored in the remaining 2 bits of the 8 bits in 1 byte.

```zig
const Node = union(enum) {
    Next,
    BranchYes: u6,
    BranchYes: u6,
    Terminal: u6,
};

print("{}",.{@sizeOf(Node)})

```

Can someone explain to me why this Zig does not make this optimization?

---

<div class="post-metadata">

**Author:** ![Plebosaur](https://ziggit.dev/user_avatar/ziggit.dev/plebosaur/32/6857_2.png) [@Plebosaur](https://ziggit.dev/u/Plebosaur)\
**Post date:** [July 31, 2026, 6:46pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/2 "2026-07-31T18:46:34Z")

</div>

There is a proposal to make this for ?enum{}, maybe they will add it at some point in the future, for now if you absolutely need the optimization you can do this:

```zig
const NodeTag = enum(u2) {
    Next,
    BranchYes,
    Terminal,
}

const Node = packed struct(u8) {
    tag: NodeTag,
    payload: packed union(u6) {
        BranchYes: u6,
        Terminal: u6,
    }
};

```

You will have to do some stuff manually, but it is what it is.

---

<div class="post-metadata">

**Author:** ![pachde](https://ziggit.dev/user_avatar/ziggit.dev/pachde/32/2485_2.png) [@pachde](https://ziggit.dev/u/pachde)\
**Post date:** [July 31, 2026, 7:24pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/3 "2026-07-31T19:24:29Z")

</div>

Since the language and libraries are changing a lot, many optimizations mostly remain in the future. This optimization is a space optimization that will likely have code size and runtime performance implications, so it’s not a 100% win, depending on circumstances and your goals.

There are issues open for this and the similar case for optionals – so-called “niche optimization”, so this area is on the radar.

I expect this and other optimization will naturally become more of a focus after we have more native backends.

---

<div class="post-metadata">

**Author:** ![redruin1](https://ziggit.dev/user_avatar/ziggit.dev/redruin1/32/9845_2.png) [@redruin1](https://ziggit.dev/u/redruin1)\
**Post date:** [August 5, 2026, 10:00am UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/4 "2026-08-05T10:00:00Z")

</div>

Is there a link to this proposal somewhere?

---

<div class="post-metadata">

**Author:** ![Plebosaur](https://ziggit.dev/user_avatar/ziggit.dev/plebosaur/32/6857_2.png) [@Plebosaur](https://ziggit.dev/u/Plebosaur)\
**Post date:** [August 5, 2026, 12:25pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/6 "2026-08-05T12:25:13Z")

</div>

I don’t remember the exact name, it’s an old proposal on github if you want to find it, not in codeberg.

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [August 5, 2026, 5:18pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/7 "2026-08-05T17:18:17Z")

</div>

> [@Plebosaur](#):
>
> There is a proposal to make this for ?enum{}, maybe they will add it at some point in the future

> <https://github.com/ziglang/zig/issues/104>
>
> Track which values are used when data sizes are bigger than necessary and use th…ose states for maybe types. For example, \`?bool\` could use value \`2\` of the bool as the null value, and \`??bool\` can use value \`3\` for the null value, and so on.
> 
> If we have spaces in structs for alignment purposes, that space could be used for the null value, or if any of the fields have null space available, such as a struct with a bool field in it somewhere.

> <https://github.com/ziglang/zig/issues/12186>
>
> A lot of exhaustive enums don't use up the full number of options for their stor…age class, thus they have leftover bits or values.
> 
> We can take advantage of these bits to reduce the memory footprint of optional values to these enums by using one of the following storage strategies:
> 
> 1. Adding a hidden enum variant \`is\_null\` that will be encoded as the last possible value in the integer space
> 2. Storing a additional bit in the MSB position for a \`is\_null\` value.
> 
> Both variants work when we define the padding bits in a non-byte integer as \`0\` which then, when accessing the optional payload would overwrite the \`is\_null\` marker with "not set", which is always ok as we're not allowed to point to the payload of a optional type anyways if that type is \`null\`.
> 
> This optimization can be implemented transparent to the user and might not be required to be put into the language spec itself, but \[it would required a change in current language semantics\](https://github.com/ziglang/zig/issues/11263#issuecomment-1075326049).
> 
> Related proposals/issues:
> \- #3806
> \- #11263
> \- #11761
> \- #104
> 
> This idea originated from a \[discussion in the Zig discord\](https://discord.com/channels/605571803288698900/719644313348341760/999591867354185789).

---

<div class="post-metadata">

**Author:** ![xgallom](https://ziggit.dev/user_avatar/ziggit.dev/xgallom/32/7367_2.png) [@xgallom](https://ziggit.dev/u/xgallom)\
**Post date:** [August 8, 2026, 1:14pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/8 "2026-08-08T13:14:39Z")

</div>

If you can achieve it with `packed struct` and `packed union`, it doesn’t really make sense to apply the optimization to the general case, as you would expect non-packed ints to still take up for example `u8` space?

---

<div class="post-metadata">

**Author:** ![giluis](https://ziggit.dev/user_avatar/ziggit.dev/giluis/32/5886_2.png) [@giluis](https://ziggit.dev/u/giluis)\
**Post date:** [August 8, 2026, 9:10pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/9 "2026-08-08T21:10:10Z")

</div>

You mean like this? Yeah, that works and makes sense, but I lose some union ergonomics

> [@Plebosaur](#):
>
> ```zig
> const NodeTag = enum(u2) {
> Next,
> BranchYes,
> Terminal,
> }
> 
> const Node = packed struct(u8) {
> tag: NodeTag,
> payload: packed union(u6) {
> BranchYes: u6,
> Terminal: u6,
> }
> };
> 
> ```

---

<div class="post-metadata">

**Author:** ![andrewrk](https://ziggit.dev/user_avatar/ziggit.dev/andrewrk/32/7011_2.png) [@andrewrk](https://ziggit.dev/u/andrewrk)\
**Post date:** [August 8, 2026, 11:01pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/10 "2026-08-08T23:01:52Z")

</div>

Niche optimization would not handle this case because as you hinted, it removes the ability to create a pointer to the tag value or to the payload value. If you want the tag to be bit packed, you have to use bitpack language features - that’s how you opt out of the requirement of being able to make a pointer into the fields of aggregates.

---

<div class="post-metadata">

**Author:** ![WeeBull](https://ziggit.dev/user_avatar/ziggit.dev/weebull/32/6828_2.png) [@WeeBull](https://ziggit.dev/u/WeeBull)\
**Post date:** [August 9, 2026, 4:34pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/11 "2026-08-09T16:34:28Z")

</div>

> [@andrewrk](#):
>
> that’s how you opt out of the requirement of being able to make a pointer into the fields of aggregates.

I think I’d question if the tag of a union has the same requirement of needing to have a separately addressable location like fields of a structure do. I think it falls under a different classification.

…but that’s a side point. I think it would be really nice if the way of opting-in/out/“all about” didn’t require the Zig code that accesses the type to be aware of the difference. That would be communicated through the type. That’s how it’s for a bit-packed struct, so it should be for a “bit-packed union”. Similar when you add alignment or padding.

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [August 9, 2026, 6:01pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/12 "2026-08-09T18:01:11Z")

</div>

It is the field which needs to be addressable not the tag. The problem is that when you are switching on the union and use a prong that captures by pointer then this pointer needs to address a single byte, but in the packed case this payload byte would also contain the 2 bits of the tag, so writing a new value to the payload would also clobber the tag value.

Or it would have to implement some special pointer type that does masked reads and writes to some bits of the location. Maybe these bit-aligned pointers could be that, but I don’t feel like I have enough information about those because they seem poorly documented.

Considering that the following works, I tend to agree that I don’t quite understand why the capture by pointer in a switch payload couldn’t be equivalent to the `ptr` in this example:

```zig
const std = @import("std");

const NodeTag = enum(u2) {
    Next,
    BranchYes,
    Terminal,
};

const Node = packed struct(u8) {
    tag: NodeTag,
    payload: packed union(u6) {
        BranchYes: u6,
        Terminal: u6,
    },
};

pub fn main() !void {
    var node: Node = .{
        .tag = .Terminal,
        .payload = .{ .Terminal = 5 },
    };

    const ptr = &node.payload.Terminal;

    const new_value: u6 = 13;
    ptr.* = new_value;

    std.debug.print("node: {}\n", .{node});
}

```

Where compileLog gives this pointer type for ptr: `*align(1:2:1) u6`

Are these bit-offset pointers documented somewhere?

---

<div class="post-metadata">

**Author:** ![gwenzek](https://ziggit.dev/user_avatar/ziggit.dev/gwenzek/32/8137_2.png) [@gwenzek](https://ziggit.dev/u/gwenzek)\
**Post date:** [August 11, 2026, 8:03pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/13 "2026-08-11T20:03:22Z")

</div>

Ultimately I think it would be nice to have language support for this. Similarly ?Enum is weird. I often feel a conflict between bit packing to optimize memory storage and the niceties of the language like switch or optional branching

---

<div class="post-metadata">

**Author:** ![Plebosaur](https://ziggit.dev/user_avatar/ziggit.dev/plebosaur/32/6857_2.png) [@Plebosaur](https://ziggit.dev/u/Plebosaur)\
**Post date:** [August 11, 2026, 11:21pm UTC](https://ziggit.dev/t/unexpected-size-of-union/17018/14 "2026-08-11T23:21:26Z")

</div>

I think it would be nice if we could type “packed union(enum)” and it does the packing, because as a default if we would basically force the abnormally aligned payload pointer into every use case.

example

```zig
const Node = packed union(enum(u2)) {
    Next,
    BranchYes: u6,
    Terminal: u6,
};

```

```zig
switch (node) {
    .BranchYes => |*tmp| {} // is not *u6, but aligned ptr 
    .Terminal => |tmp| {}, 
    .Next => {},
}

```
