Notes

A Guide to Modern C

A completely unrelated picture of some mountains in Kelowna with a hotel in the foreground

A ton of C codebases and tutorials use geriatric practices for reasons related to portability, compatibility, and nostalgia. I get it, but C has come a long way since then, so I’ve made a list of some good stuff you might be missing out on.

By the way, this page is for new C programmers. If you have already worked on a few projects, you most likely know about everything mentioned. This is a short list, and you can learn way more by checking out cppreference.com or some of the other resources.

Defer keyword

I am beginning with the big and controversial one. There is a technical specification (TS 25755) for a defer keyword in C. Although it is not currently part of the standard itself, it is in the current release of Clang. There is a patch for GCC as well, but I don’t know how it’s going along.

If you’re not sure what a defer statement does, it just causes code to execute when the current code block finishes. For example, lot of C projects use the goto statement for cleanup.

Cleanup with goto
int foo() {
  int ret = 0;
  struct foo_s foo;
  if (foo_init(&foo) != 0)
        return 1;
  if (foo_bar(&foo) != 0) {
    ret = 1;
    goto end;
  }
  if (foo_baz(&foo) != 0) {
    ret = 1;
    goto end;
  }
  end:
  foo_finish(&foo);
  return ret;
}

With defer, you can do something like this instead.

Cleanup with defer
int foo() {
  int ret = 0;
  struct foo_s foo;
  if (foo_init(&foo) != 0)
    return 1;
  defer foo_finish(&foo);
  if (foo_bar(&foo) != 0)
    return 1;
  if (foo_baz(&foo) != 0)
    return 1;
  return 0;
}

At least for me, this is much easier to stay on top of, especially for longer functions.

Compiler support isn’t that widespread yet, but it is usable with a compiler flag as of Clang 22, so you can use it in Fedora 44, Gentoo, and (probably) your favourite rolling-release distribution. There are also a few macro tricks that can be used lacking compiler support.

Cleanup attributes

It’s not part of ISO C, but all compilers worth their salt support the cleanup attribute that basically lets you run a callback when a variable goes out of scope. However, a lot of people don’t use it for whatever reason, so it’s made the list. I’ll include a full example so you can try this in your compiler of choice.

Cleanup attributes
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

void free_cb(uint8_t **ptr) {
  free(*ptr);
}

int main() {
  __attribute__((cleanup(free_cb))) uint8_t *data = calloc(1, 4);
  data[0] = 'a';
  data[1] = 'b';
  data[2] = 'c';
  fprintf(stdout, "Result: %s\n", data);
}

If you run it with valgrind, you’ll notice that there are no memory leaks. You might notice that this mostly handles the same use case as defer, and you would be correct. However, unlike defer, compilers have supported cleanup attributes as a non-standard feature for decades, so it is way more common.

In addition, note that there’s no “return 0;” at the end of the function either. That is not a mistake. Among other special properties, the main function automatically returns 0 at the end.

Fixed-width types

For misguided historical reasons, the number of bytes in the C data types is implementation-dependent, so sizeof(int) isn’t guaranteed to be 4 bytes, nor is sizeof(char) guaranteed to be 1 byte. The actual sizes are defined by the platform instead.

This is rarely a problem in practice, but being able to explicitly specify the size of a data type is nice. This is what fixed-width types (stdint.h) are for. They’ve been around since C99 and let you explicitly specify how many bits are in a number. Pretty much all modern codebases use these types at this point, but if you didn’t know about them, now you do.

__FILE__ and __LINE__ macros

The C preprocessor has __FILE__ and __LINE__ macros that, as described on the tin, expand to the file and line names. This is very useful for producing verbose error messages. For example, I often find myself doing something like below.

#define verbose_printf(...)                                                    \
  {                                                                            \
    fprintf(stderr, "%s:%d: ", __FILE__, __LINE__);                            \
    fprintf(stderr, __VA_ARGS__);                                              \
  }

This is useful for obvious reasons. I’ve seen people hardcode numbers in their print statements so they can identify where their programs failed, and it’s gross. Please do this instead.

Takeaway

One of the big advantages of C is that it’s small and portable. You can hammer out 1000 lines of code without needing to think about language features, and you can compile for almost anything. The code doesn’t even have to be good — many people in engineering will claim to know programming better than most programmers while cluelessly perpetrating heinous crimes against their poor (usually C and Python) toolchains, but I digress.

I completely understand why a lot of people are averse to anything that might complicate the language or reduce the compiler support of their program. There are many programmers who will say “just switch to C++ / Rust / Zig if you want nice things”, and they have a point, but it’s usually unhelpful. A ton of major projects (Linux included) make a point of using modern practices in their C codebases, and I hope that people who are learning C consider doing the same.

Other resources