21st Century C



Itay Garin

Itay is a developer and a researcher who's passionate about solving low-level challenges. He loves to tinker with electronics, to optimize his Emacs config, and to learn new technologies.


21st Century C

Posted by Itay Garin on .

10 modern C tips and tricks that will make your code better


I recently stumbled upon the 22nd century C article on HackerNews.

The article highlighted many exotic C features that are probably unknown to many. Admittedly, there were a few gems that I wasn’t aware of myself.

So I thought it would be a good idea to share these insights with you. Also, the author didn’t provide any commentary for his suggestions so it felt like they could use some interpretation.

Naturally, I didn’t agree with all of his suggestions. So I took the liberty of including only the points I liked. Of course, I’ve included a few of my tricks as well :)

Finally, you don’t need to take notes on this one. I’ve extracted the recommendations into a reference header file - 21st.h. Enjoy!

My Setup

Before diving into the list of features, I should describe my compilation setup -

I tested these features on an Ubuntu 16.04 machine with gcc 5.4 and clang 3.8. By default, both of these compilers use the c11 standard.

These features are not that new, so it’s almost certain that they’ll work on your setup.

Types and Variables Tweaks


#include <stdbool.h>

An unfortunate yet common practice among C developers is defining their own bool type. I know I’ve been guilty of this.

I suspect that people are still doing that because the definition only appeared in the later c99 standard. So, in case you haven’t heard, the standard boolean type can be obtained via the stdbool.h header.

By including this header, you get the bool typedef as well as the false and true preprocessor definitions. Under the hood, bool is represented as an unsigned byte. Internally, this type is called _Bool.

Also, modern compilers provide an awesome enforcing mechanism for this type.

Here’s Wikipedia’s explanation -

_Bool functions similarly to a normal integral type, with one exception: any assignments to a _Bool that are not 0 (false) are stored as 1 (true). This behavior exists to avoid integer overflows in implicit narrowing conversions.

Here’s a short code sample that illustrates this behavior -

uint8_t b1 = 256;
bool b2 = 256;

if (b1) {
    /* won't happen... */
if (b2) {
    /* will happen */

Rust-like types

#include <inttypes.h>

typedef int8_t   i8;
typedef uint8_t  u8;
typedef int16_t  i16;
typedef uint16_t u16;
typedef int32_t  i32;
typedef uint32_t u32;
typedef int64_t  i64;
typedef uint64_t u64;


u64 my_var = 42;

One of the little things I missed from Rust is its compact and ergonomic type names. These names convey the same meaning as the verbose counterparts in less than half of the characters.

You may have noticed that I haven’t included the definitions of c8, f32 and f64 that were in the original article. Unfortunately, The size of C’s char, float and double may be inconsistent across different systems. It felt wrong to ignore these potential pitfalls.

Similarly, I’ve omitted the b8 definition. Again, the standard doesn’t guarantee that bool will be represented as a byte. Moreover, In my opinion, bool represents more of a logical data structure than a numerical one. Thus, it felt weird to group it with the other integer types.

Of course, my opinions on this matter are debatable. I encourage you to try these definitions for yourself and see what works for you!

_ - Anonymous Naming

#define _Merge(x, y) x##y
#define _Anyname(x) _Merge(_Anyname_, x)
#define _ _Anyname(__COUNTER__)


_ = ignored_result();

This is a fun one that I haven’t seen anyone else attempt to do in C. The _ typedef provides a unique anonymous-like variable name.

That’s accomplished with the help of the __COUNTER__ macro. This macro expands to sequential integral values starting from 0.

Honestly, I couldn’t think of many cases where this would be useful. Nevertheless, it is a neat trick to have in your arsenal.

var and let - Type Inference

#define var __auto_type
#define let __auto_type const


var i = 0;
let pi = 3.14;

This is another minor feature that I’ve come to miss from Rust. That is the ability to define variables without explicitly specifying their type. Again, this is an ergonomic key-strokes saver that can make our lives a little bit easier.

Type inference is a very common feature in today’s modern languages. Fortunately, a similar effect can be implemented in C-land.

Though, keep in mind that in C we often need to pay careful attention to the characteristics of our data structures. There are cases where ambiguity in the size or sign of a type may lead to vulnerabilities.

Compiler Extensions

care - Warn about Unused Results

  #define care __attribute__((warn_unused_result))

  care error_t critical_function(void);

The unused result attribute is a safety measure that’s worth knowing. Undoubtedly, it’s less useful than Rust’s error wrapping facilities, but it’s still useful indeed.

In my setup the -Wunused-result is enabled by default. If you’d like to be stricter, you can transform this warning into an error with the -Werror flag.

defer - Automatic Deconstruction

#define defer(x) __attribute__((cleanup(x)))


void destructor(my_object_t * obj) {

int main(void) {
    defer(destructor) my_object_t obj = constructor();
    return 0;

} // destructor(&obj) - will be called

Initially, C was missing a standard way to defer the orderly destruction of objects when their lifetime ends. When compared to C++’s destructors this issue becomes very apparent.

Fortunately, modern C compilers introduced the cleanup attribute to fill this hole.

With that said, I must warn you that this won’t work under abnormal exit conditions. Specifically, this feature won’t work in conjunction with calls to abort, exit and longjump.

Macros and Utilities

Control Flow Macros

#define RET_IF_TRUE(expression, retval)         \
  if((expression)) {                            \
    return (retval);                            \

#define RET_IF_FALSE(expression, retval) ...
#define RET_IF_NULL(expression, retval) ...
#define GOTO_IF_NULL(expression, retval) ...
#define GOTO_IF_TRUE(expression, retval) ...
#define GOTO_IF_FALSE(expression, retval) ...


print_err_t print(const char * string) {
    RET_IF_NULL(string, ERR_NULL_STR);
    RET_IF_FALSE(is_str_len_valid(string), ERR_INVALID_STR_LEN);
    RET_IF_TRUE(has_non_printable_chars(string), ERR_BAD_CHARS_IN_STR);
    return SUCCESS;

These utility macros are personal additions of mine. I honestly grew tired of opening a new if block every time a validity check was necessary. These macros make this task much less annoying.

In my opinion, it also makes C code more concise, compact and visually pleasing.

for Utility Macros

#define forcount(index, count) \
    for(size_t index = 0, size = count; index < size; ++index)

#define foruntil(index, end, array) \
    for(size_t index = 0; (array)[index] != end; ++index)

#define forrange(index, start, end) \
    for(size_t index = start, stop = end; index != stop; ++index)

forcount(i, 10) {
    printf("i = %d\n", i);

How haven’t I thought of these macros? They are so simple yet so handy.

They are pretty self-explanatory, so I’ll let you figure them out for yourself. Of course, you can customize these macros to fit your own style and taste.

Compilations Recommendations

-Wall Compilation

This one isn’t an exotic feature like the previous ones. Still, I felt it’d be appropriate to include it.

The additional warnings provided by -Wall protected me on numerous occasions. They surely spared me many long and dreadful debugging sessions.

I encourage you to adopt this flag as a mandatory flag in your projects. If you’d like to go the extra mile, you should also consider -Wextra and -Werror.

Static Analysis

function clangs {
  clang --analyze -Xanalyzer -analyzer-output=text $@ && clang $@;

clangs -o app main.c -std=c99 -fsanitize=leak

This is a golden nugget that’s hidden quite well in the original article. Similarly to the -Wall and -Wextra warnings, it’s worth baking such static checks into your compilation process.

The outlined clangs command is just one way to accomplish that. If you prefer, you could simply add the static check to your Makefile to achieve the same effect.


Itay Garin

Itay is a developer and a researcher who's passionate about solving low-level challenges. He loves to tinker with electronics, to optimize his Emacs config, and to learn new technologies.