DEV Community

loading...

Expression-Oriented Programming in C: The FMT Macro

hirrolot profile image Temirkhan Myrzamadi ・3 min read

Have you ever created useless intermediate variables like this?

int fee = 500;

char response[128] = {0};
sprintf(response, "Your fee is %d USD", fee);

reply_to_user(response);
Enter fullscreen mode Exit fullscreen mode

If you programmed in C before, the answer should be "yes". This language design drawback arises from the fact that C is a statement-oriented language, meaning that if you want to perform some simple task, big chances that you need to allocate a separate variable and manipulate its pointer here and there.

This blog post explains how to overcome this unpleasantness by designing APIs which facilitate expression-oriented programming. In the end, we will come up with a handy FMT string formatting macro that mimics std::format! of Rust:

int fee = 500;
reply_to_user(FMT((char[128]){0}, "Your fee is %d USD", fee));
Enter fullscreen mode Exit fullscreen mode

sprintf

Why sprintf (and its friends) requires a named variable to store a formatted string? Well, let us envision sprintf in an ideal world where every meaningful operation fits in a single line -- how would then the signature look? Something like this:

char *sprintf(const char *restrict format, ...);
Enter fullscreen mode Exit fullscreen mode

(Note: restrict here means that an object referenced by format will be accessed only through format.)

But the gross truth is that it is completely unviable in C: this sprintf then needs to allocate memory by itself, whilst many use cases require caller-allocated memory. Let's fix it:

char *sprintf(char *restrict buffer, const char *restrict format, ...);
Enter fullscreen mode Exit fullscreen mode

Now the signature is the same as the standard library counterpart, except that we return the passed buffer instead of how many bytes have been written so far. Good. But no one uses it anyway, so we can freely get rid of it. Let's then define our superior sprintf wrapper.

Superior sprintf

#include <stdarg.h>
#include <stdio.h>

#define FMT(buffer, fmt, ...) fmt_str((buffer), (fmt), __VA_ARGS__)

inline static char *fmt_str(char *restrict buffer, const char *restrict fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    vsprintf(buffer, fmt, ap);
    va_end(ap);

    return buffer;
}
Enter fullscreen mode Exit fullscreen mode

See? What we have done is just paraphrasing the old but not obsolete sprintf: now we can obtain the resulting string immediately from an invocation of FMT, without auxiliary variables:

char *s = FMT((char[128]){0}, "%s %d %f", "hello world", 123, 89.209);
Enter fullscreen mode Exit fullscreen mode

If you are curious about (char[128]){0}, it is called a compound literal: it represents an lvalue with the automatic storage duration that is needed only once. Here, the type of the compound literal is char[128], a char array of 128 elements, all initialised to zero.

The snprintf counterpart

We can modify our FMT and fmt_str to use the safe alternative to sprintf, snprintf:

#include <stddef.h>
#include <stdarg.h>
#include <stdio.h>

#define FMT(buffer, fmt, ...) fmt_str(sizeof(buffer), (buffer), (fmt), __VA_ARGS__)

inline static char *fmt_str(
    size_t len, char buffer[restrict static len], const char fmt[restrict],
    ...) {
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buffer, len, fmt, ap);
    va_end(ap);

    return buffer;
}
Enter fullscreen mode Exit fullscreen mode

Now if the passed buffer is not sufficient to hold the formatted data, we will not write past the end of it. Notice how beautiful sizeof(buffer) inside FMT works: provided it is an array type, its size will be computed correctly, contrary to just a pointer to the first element of an array.

If you are curious about the char buffer[restrict static len] syntax, it defines a restrict pointer parameter that points to the first element of some array whose length is at least len bytes long.

Conclusion

What can we learn from it? At least, there is a certain kind of functions that write to a memory area, and their invocation naturally expresses the whole result of an operation. In order to be suitable for expression-oriented programming, these must return the passed memory area. FMT is a perfect example.

Compound literals facilitate expression-oriented programming too. They represent arbitrary values that need to be accessed only once. I encourage you to use them in all cases where a separate variable is superfluous. (Although you might still prefer to give descriptive names to program entities in certain cases.)

I hope you enjoyed the post. Feel free to give your feedback and support me on Patreon.

Discussion (5)

Collapse
gabrielfallen profile image
Alexander Chichigin

Great post! It was super interesting to learn about compound literals. Maybe you can comment on restrict everywhere too? Especially applied to array lengths? Thanks! 😃

Collapse
hirrolot profile image
Temirkhan Myrzamadi Author • Edited

Thanks for your feedback. Commented on the restrict and static keywords too.

Collapse
gabrielfallen profile image
Alexander Chichigin

Do I get it right that these restrict and array length annotations are just promises from a programmer and compiler don't and can't check their actual validity?

Thread Thread
hirrolot profile image
Temirkhan Myrzamadi Author

Sort of. Formally, they are promises, though some sane compilers can check a passed array length: hamberg.no/erlend/posts/2013-02-18....

Thread Thread
gabrielfallen profile image
Alexander Chichigin

Cool! Thanks. 😃

Forem Open with the Forem app