Search code examples
arraysctype-punning

Is there a way to have overlapping runtime-sized arrays of different types in C?


I would like to have a runtime-sized buffer (allocated via malloc() for example) where I can safely access any sufficiently aligned/sized slot as either a uint8_t uint16_t uint32_t or uint64_t. An implementation of this for a fixed-sized buffer would be fairly straightforward where N is a compile-time constant:

union Buffer {
    uint8_t u8[N * 8];
    uint16_t u16[N * 4];
    uint32_t u32[N * 2];
    uint64_t u64[N];
};

An alternative approach is to have each unit as a union and then allocate an array of these:

union Buffer {
    uint8_t u8[8];
    uint16_t u16[4];
    uint32_t u32[2];
    uint64_t u64;
};

But this approach has shortcomings as described in a different question I asked. Most notably:

  • The buffer is not guaranteed to not have padding between the successive union elements.
  • One cannot safely cast the buffer into one a pointer to one of uint8_t uint16_t uint32_t uint64_t and then index the pointer beyond the boundary of the first element.

So to summarize:

  • I want overlapping (sharing the same memory) arrays of uint8_t uint16_t uint32_t and uint64_t where I can safely obtain any sufficiently aligned slot in the array as an lvalue of any of the above types.
  • The object is runtime-sized and allocated via malloc()/realloc()/etc.
  • No strict-aliasing violations or any undefined behavior.
  • All elements are contiguous.

Is there any way to do what I described? Or is the only way to use memcpy() to safely read/write the elements as the desired types?


Solution

  • If you never want to read memory as a different type than you previously wrote it, then this is trivial. Dynamically allocated memory is intended to be flexible. You do not need arrays at all. Simply convert the pointer to the memory to a pointer to the desired type and use it. Given pv a void * that has been assigned from malloc:

    // Convert pointer.
    uint16_t *pu16 = p;
    
    // Write element.
    pu16[i] = 3;
    
    // Read element.
    uint16_t x = pu16[i];
    

    Later, you could write the same memory as uint32_t or other types. As long as you do not write as one type and read as another, this is defined by the C standard, because the effective type for a write to dynamically allocated memory is the type of the lvalue used to access it, and the effective type for a read is the type last used to write to it, except that character types may always be used and do not change the effective type.

    The array arithmetic is guaranteed by the specification for malloc et al in C 2024 7.24.4.1: “The pointer returned if the allocation succeeds is suitably aligned so that it can be assigned to a pointer to any type of object with a fundamental alignment requirement and size less than or equal to the size requested. It can then be used to access such an object or an array of such objects in the space allocated (until the space is explicitly deallocated).”

    If you do need to read memory as a different type than it was written, and not a character type, then you could use memcpy to copy the bytes from the memory into an object of the desired type.