'How to create 24 bit unsigned integer in C

I am working on an embedded application where RAM is extremely tight. For this purpose I need to create a 24 bit unsigned integer data type. I am doing this using a struct:

typedef struct
{
    uint32_t v : 24;
} uint24_t;

However when I interrogate the size of a variable of this type, it returns "4", i.e.:

    uint24_t x;
    x.v = 0;
    printf("Size = %u", sizeof(x));

Is there a way I can force this variable to have 3 bytes?

Initially I thought it was because it is forcing datatypes to be word aligned, but I can for example do this:

typedef struct
{
    uint8_t blah[3];
} mytype;

And in that case the size comes out at 3.



Solution 1:[1]

Well, you could try to ensure that the structure only takes up the space you need, with something like:

#pragma pack(push, 1)
typedef struct { uint8_t byt[3]; } UInt24;
#pragma pack(pop)

You may have to provide those compiler directives (like the #pragma lines above) to ensure there's no padding but this will probably be the default for a structure with only eight-bit fields(a).

You would probably then have to pack/unpack real values to and from the structure, something like:

// Inline suggestion used to (hopefully) reduce overhead.

inline uint32_t unpack(UInt24 x) {
    uint32_t retVal = x.byt[0];
    retVal = retVal << 8 | x.byt[1];
    retVal = retVal << 8 | x.byt[2];
    return retVal;
}
                                      
inline UInt24 pack(uint32_t x) {
    UInt24 retVal;
    retVal.byt[0] = (x >> 16) & 0xff;
    retVal.byt[1] = (x >> 8) & 0xff;
    retVal.byt[2] = x & 0xff;
    return retVal;
}

Note that this gives you big-endian values regardless of your actual architecture. This won't matter if you're exclusively packing and unpacking yourself, but it may be an issue if you want to use the memory blocks elsewhere in a specific layout (in which case you can just change the pack/unpack code to use the desired format).

This method adds a little code to your system (and a probably minimal performance penalty) so you'll have to decide if that's worth the saving in data space used.


(a) For example, both gcc 7.3 and clang 6.0 show 3 6 for the following program, showing that there is no padding either within or following the structure:

#include <stdio.h>
#include <stdint.h>

typedef struct { uint8_t byt[3]; } UInt24;
int main() {
    UInt24 x, y[2];
    printf("%zd %zd\n", sizeof(x), sizeof(y));
    return 0;
}

However, that is just a sample so you may want to consider, in the interest of portable code, using something like #pragma pack(1), or putting in code to catch environments where this may not be the case.

Solution 2:[2]

A comment by João Baptista on this site says that you can use #pragma pack. Another option is to use __attribute__((packed)):

#ifndef __GNUC__
# define __attribute__(x)
#endif
struct uint24_t { unsigned long v:24; };
typedef struct uint24_t __attribute__((packed)) uint24_t;

This should work on GCC and Clang.

Note, however, that this will probably screw up alignment unless your processor supports unaligned access.

Solution 3:[3]

Initially I thought it was because it is forcing datatypes to be word aligned

Different datatypes can have different alignment. See for example the Objects and alignment doc.

You can use alignof to check, but it's totally normal for char or uint8_t to have 1-byte (ie, effectively no) alignment, but for uint32_t to have 4-bye alignment. I don't know if the alignment of bitfields is explicitly described, but inheriting it from the storage type seems reasonable enough.

NB. The reason for having alignment requirements is generally that it works better with the underlying hardware. If you do use #pragma pack or __attribute__((packed)) or whatever, you may take a performance hit as the compiler - or the memory hardware - silently handle misaligned accesses.

Just explicitly storing a 3-byte array is probably better, IMO.

Solution 4:[4]

To begin with, don't use bit-fields or structs. They may include padding as they please and bit-fields are non-portable in general.

Unless your CPU explicitly got 24 bit arithmetic instructions - which doesn't seem very likely unless it's some oddball DSP - then a custom data type will achieve nothing but extra stack bloat.

Most likely you will have to use uint32_t for all arithmetic. Meaning that your 24 bit type might not achieve much when it comes to saving RAM. If you invent some custom ADT with setter/getter (serialization/de-serialization) access, you are probably just wasting RAM since you get higher stack peak usage if the functions can't be inlined.

To actually save RAM you should rather revise your program design.


That being said, you can create a custom type based on an array:

typedef unsigned char u24_t[3];

Whenever you need to access the data, you memcpy it to/from a 32 bit type and then do all arithmetic on 32 bits:

u24_t    u24;
uint32_t u32;
...
memcpy(&u32, u24, sizeof(u24));
...
memcpy(&u24, &u32, sizeof(u24));

But note that this assumes little endian, since we are only working with bits 0 to 2. In case of a big endian system, you will have to do memcpy((uint8_t*)&u32+1, ... to discard the MS byte.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 thomas
Solution 2
Solution 3 Useless
Solution 4 Lundin