Comparing structs in C
What do you think this C code prints?
struct foo first = { 0 };
struct foo second = { 0 };
if (memcmp(&first, &second, sizeof(first)) == 0) {
printf("equal\n");
} else {
printf("not equal\n");
}
The corrent answer is: “I don’t know.”
The reason is that we don’t know if the struct contains padding bytes inserted by the compiler. Because if it does, those bytes might not be initialized. The only way to set them is with memset
.
So if the struct was defined like this
struct foo {
uint8_t a;
uint32_t b;
};
The compiler would turn it into this
struct foo {
uint8_t a;
uint8_t _padding[3]; // automatically added by compiler
uint32_t b;
};
Therefore, after our initialization above, the structs might look like this in memory
first 00 aa bb cc 00 00 00 00
second 00 dd ee ff 00 00 00 00
That’s not quite equal, is it?
We could add __attribute__((packed))
to prevent the padding bytes from being inserted, but let’s just assume we can spare a few bytes of memory and avoid having to deal with unaligned (slow) memory accesses.
Comparing structs with a simple memcmp
is, however, quite convenient, so it’s a good thing this little hiccup can easily be solved – we just add the padding ourselves!
struct foo {
uint8_t a;
uint8_t _padding[3]; // added by us, not the compiler!
uint32_t b;
};
Now, when that’s initialized with { 0 }
, we do in fact get
first 00 00 00 00 00 00 00 00
second 00 00 00 00 00 00 00 00
But there is still a problem: at some point we might change the struct so that more padding is needed… and forget to add it. Then we’d have an annoying, transient bug. Oops.
Fortunately, if we enable the -Wpadded
warning (or error, with -Werror=padded
), the compiler yells about every automatic padding it adds.
It’s probably not a good idea to enable that globally though, unless we want to spend the next few months fixing all the issues, so it’s better to only apply it to structs that we actually want to use memcmp
with, like this:
#pragma GCC diagnostic push
#pragma GCC diagnostic error "-Wpadded"
struct foo {
uint8_t a;
uint8_t _padding[3];
uint32_t b;
};
#pragma GCC diagnostic pop
PS. When dynamically allocating, remember to either memset
to zero or just allocate with calloc
.
Alternative: Serialize & Compare
Another way to compare structs is to serialize them and then compare the resulting memory. It might be a little worse for performance, but it’s probably okay in most cases.
For example, AutoPTT already uses Protobuf for its settings and IPC, so it was quite convenient to just use serialization for comparing.
bool settings_v3_equal(
struct settings_v3_s const *a,
struct settings_v3_s const *b) {
if (a == b) {
return true;
}
if (a == NULL || b == NULL) {
return false;
}
struct fatptr_s a_serialized = settings_v3_serialize(a);
struct fatptr_s b_serialized = settings_v3_serialize(b);
bool equals = a_serialized.len == b_serialized.len && memcmp(
a_serialized.ptr,
b_serialized.ptr,
a_serialized.len
) == 0;
free(a_serialized.ptr);
free(b_serialized.ptr);
return equals;
}
AutoPTT is also built with MSVC instead of GCC or Clang (though I do use Clang for its LSP), and MSVC does not support the warnings presented above.