Skip to content

Instantly share code, notes, and snippets.

@SasLuca
Created January 18, 2024 15:54
Show Gist options
  • Save SasLuca/88f4a93de5dd007d9b2e58a0420cd6cc to your computer and use it in GitHub Desktop.
Save SasLuca/88f4a93de5dd007d9b2e58a0420cd6cc to your computer and use it in GitHub Desktop.
Thoughts on NULL in Modern C

There are multiple styles you can write modern C in and thus multiple ways to reason and deal with NULL. You have to on some level reason about your use case, patterns and audience.

How you write this might be affected for example by whether you are writing the code just for yourself, a team of a certain size with a certain level of shared understanding, or a public library targetting a certain audience.

Documentation

Documentation wise, I think going with the assumption that a pointer is not nullable by default is a good strategy, so if you are writing a function that takes optional pointers you should probably document that, even if it's by something simple like a naming convetion (eg: a prefix like opt_ or optional_).

Assertions

Assertions I think are generally good for things that make the program close to irecoverable. A common example of this I think is a memory allocation. If I make my game budgeting for 1GB of RAM and the player's device just doesn't have that I can just assert it and in a global handler try and explain to the user that they simply lack the necessary resources to run the game, and then exit. Checking every single malloc call for null is otherwise kinda silly given that you cant do much with that information at a malloc call site. In languages like Zig and Rust you get an exception-like rewind mechanism but only for exceptional cases like this and not for silly stuff like FileNotFound which should obviously just be an error code.

API design

API design wise there is another interesting choice here because what you can do is write your apis in a zero-is-initialization kinda way and always handle those cases.

Here is an example using a string api:

struct str { char* data; int size; }

str a = {0};
str b = str_split_n(a, " ", 0); // Get the nth result of splitting `a` by " "
print("Result: {str}", b);

This api could crash when str_split_n is called or it could just work as normal, str_split_n returns a str{0} because it handles the null case, and print handles the null case as well so you just get "Result: null" or "Result: 0" or "Result: " printed. I think in fact this is likely preferable in such an api.

Lets take another api tho:

str file_path = {0}; // path is null
image_t img = load_image(file_path); // we load an image from disk by decompressing it from png to rgba8 or something
texture_t texture = load_texture(image); // we try and create a gpu texture using the bytes of that image
...
render(texture, location); // later on we render the texture

Here, crashing is also an option, but rather inconvenient. More often we would like to handle null cases, just let things continue as normal, and when we get to some operation like render we either render nothing or better yet render a default texture that we assert is always loaded in. This is kinda common in game engines for example.

Additionally what you can do here, but wouldnt make much sense in say the string api, is that you can Log things such as the image not loading because file path is null, and log that the texture couldnt be created because the image was empty, and log that the render call used the default texture for this reason (tho maybe at this point that last one is not necessary).

The reason for logging here is that this is an unexpected case, most likely an error, and you wanna help developers catch it easily. With the string api i dont think this makes sense because you might be dealing with zeroed strings at times intentionally, as many operations can just return the zeroed struct, so logging there is not really needed.

Use less pointers

This is definitely more of a modern C advice that I think is very important. Avoid using too many pointers, returns and accept structs as parameters instead, that's way way better and helps with reasoning about such issues. If you look back at the examples I gave you, you can definitely see this in action. I am not really doing pointer stuff anywhere, and that's great!

Using optional types in C23

If you like living in the bleeding edge, in C23 it will be possible to write generic types like Option(T), so that could become a useful tool for some modern C devs as well.

Here is some stuff on that:

Conclusion

So yeah, I think these are my main thoughts on this topic off the top of my head. There might be more considerations that I forgot to talk about here tho.

Some other articles and sources I recommend on this:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment