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.

Tags: