Zero-sized types vs tuple memory layout

We’ve had it stated before that a homogenous tuple has the same memory layout as a C fixed-size array of values. This is 100% true for C types, whose size is always positive and equal to stride, but for types where that’s not true the discrepancy can be observed:

struct TailPadding {
    var x: UInt16 = 0
    var y: UInt8 = 0
}

func test<T>(_ value: T) {
    var tuple = (value, value)
    var array = [value, value]
    withUnsafeMutableBytes(of: &tuple) { tupleBytes in
        array.withUnsafeMutableBytes { arrayBytes in
            print("2x", T.self, tupleBytes.count, arrayBytes.count)
        }
    }
}

test(1 as Int32) // 2x Int32 8 8
test(TailPadding()) // 2x TailPadding 7 8

This isn’t usually a huge deal; even if you’re doing weird pointer things, each element is at the same offset in both representations, and the “past-the-end” pointer is in the same place too. You could get in trouble copying from one buffer to the other, though.

The problem is zero-sized types, which have a stride of 1 (by fiat, but also to avoid weird behavior in generic implementations of low-level collections like Array).

struct Empty {}
test(Empty()) // 2x Empty 0 2

Now it’s not so clear what the address of “the second element” is. According to a pointer formed from the key path \.1, it’s 0 bytes ahead of the base pointer; according to a pointer rebound to Empty (from (Empty, Empty)), it’s 1 byte ahead (because of the stride). Elementwise operations might still get away safely because no actual reads or writes are done, but formally there’s a problem here. The stride-offset pointer might end up pointing to an address that could not be anticipated. And like the tail-padding case, trying to do a bulk operation might run into trouble from this mismatch.

I’m not exactly sure what the action item is here; the tuple/element rebinding rule is, as far as I know, still in the fuzzy realm of “described on this forum”, not specified precisely in an evolution proposal or written down in the official documentation. The simplest thing to say would be “a homogenous tuple has the same layout as a sequential array of its elements, less tail padding, unless the type has a size of 0.” But maybe that’s a poor idea for other reasons.

4 Likes