If I had to name just one characteristic that well written code should have, I had no doubt:
Well written code use the right level of abstraction
This has to do with correctly model the problem, structure your data so that it clearly represents your domain of interest, design the functions so that they reflect the chosen solution strategy, etc etc.
In the end, it has to do with the way you structure and present your code to the reader.
Remember that source code is meant to be read by humans! Computers only need 1's and 0's, the reason we have high level languages is that we, as humans, need something more expressive to communicate to the computer (and to other humans) the logic of our programs.
Readers need a story to follow, if your code is arranged in a way whose logic is not clear to the reader, it will be a nightmare for him to understand what it is all about.
Besides our desire to be understood by others, there is an old saying that we should all keep in mind:
Write your code as if the next maintainer is a violent psychopath that knows where you live.[ref]
Languages limits
How well you can write code also has a lot to do with how much your programming language allows you to express yourself.
Consistent and meaningful naming is very important, but if the language you're using only allows for one-letter variable names, you're pretty limited!
If the logic of your code is buried in hundreds of lines of boilerplate code, if you're forced to split elements that are logically related across many different source files, it means that the language is working against you instead of supporting you.
There are other situations (e.g. you had to unroll some complex loop which made your code a mess) that can require you to find a way to make it clear what you meant and make the important code evident to the (human) reader.
C Expressiveness
As a C programmer, you have some tools at your disposal: you have few limitations for functions and variables names, you can segregate their scope so names won't clash, you can use function pointers to write generic code, you can rearrange the code quite freely and so on.
However, being C a rather low-level language and being born in an era where these aspects were secondary, sometimes you have to take some extra steps to make the code clearer.
I do claim that the most powerful tool you have at your disposal is using the (in)famous C macros
.
C Macros as constants
The most common and widely accepted use for macros is to name significant constants in your code.
Instead of this:
char my_string[21];
... many lines of code after
// Initialization
for (int i=0; i<20; i++);
my_string[i]='-';
my_string[20] = '\0';
that gives no hint on why you stop at 20 (there might be tons of code in between or, the definition of my_string
being in a completely different file) you may write:
#define MY_STR_MAXLEN 20
char my_string[MY_STR_MAXLEN+1];
... many lines of code after
// Initialization
for (int i=0; i<MY_STR_MAXLEN; i++);
my_string[i]='-';
my_string[MY_STR_MAXLEN] = '\0';
or (if you want to emphasize the space available rather than the max length of the string) you may write:
#define MY_STR_MAXSIZE 21
char my_string[MY_STR_MAXSIZE];
... many lines of code after
// Initialization
for (int i=0; i<MY_STR_MAXSIZE-1; i++);
my_string[i]='-';
my_string[MY_STR_MAXSIZE-1] = '\0';
You made it easier to maintain, easier to understand, and, overall, better.
Naming constants is useful but not very exciting (and we have also other ways of doing this with const
or through enum
s).
C macros as functions
Function-like macros are somewhat controversial. They are often dismissed saying that they can be replaced with a function (which is true but not always) and that they have an issue in handling arguments.
Functions are not necessarily better
Here is one of the macros that (in a form or another) I often have in my code:
#ifndef NDEBUG
#define dbgprintf(...) \
(fprintf(stderr,__VA_ARGS__), \
fprintf(stderr," %s:%d\n",__FILE__,__LINE__) \
)
#else
#define dbgprintf(...)
#endif
it's equivalent to an fprintf()
on stderr
that will disappear completely when the code is compiled with NDEBUG
defined (as assert()
does). I'm just too lazy to go hunting all those printf() I put in the code "just to check what's going on here".
Could I have written it as a function? Yes, something like:
#include <stdarg.h>
int dbgprintf(char *file, int line, char *fmt, ...)
{
// handle va_list and use vfprintf()
}
It is much more complicated and difficult to use (and still I would need a macro to remove it when NDEBUG
is defined). In the end, less clear.
Functions-like macros are also extremely useful in handling generic functions so to create a clearer API regardless of the type of arguments.
Beware the args!
The dangerous aspect of function-like macros in handling their arguments can be easily avoided by abiding these two rules:
1 - Macro arguments must never be evaluated twice
The classical example is max()
:
#define max(a,b) ((a>b)a:b)
everything seems to work fine until you pass arguments that have side effects:
max(X++,Y++)
becomes
((X++ > Y++)? X++ : Y++)
which is clearly not what you wanted (the variables are incremented twice).
2 - Arguments should be enclosed in parenthesis
Again, the classical example is about expressions:
#define twice(a) (a*2)
you happily use it with twice(x)
or twice(f(y))
but things go south when you write:
twice(2+3)
and instead of getting 10, you'll get 8 because the macro expands to:
2+3*2
This is due to expression precedence rules, C has a lot of them and you should always ensure that your macro expands to what you expect.
The proper way to write the twice()
macro would be:
#define twice(a) ((a)*2)
Crafting functions
Before doing it, consider that you can inline a function in the code with static inline
. So, if your only reason to use macros is to "avoid calling a function", you may want to consider inlining it first!
Once you decided that a function-like macro is the way to go, you should remember that, in its essence, it is just an expression.
To craft complex functions-like macros (possibily with side effects!) you can use some of the feature of C expressions:
- Assignments are expressions
- Comma separated expressions are expressions
- The ternary operator
(?:)
is your friend - Boolean
&&
and||
are your friends too
Revisiting the example of the debugging macros:
#define dbgprintf(...) \
(fprintf(stderr,__VA_ARGS__), \
fprintf(stderr," %s:%d\n",__FILE__,__LINE__) \
)
#define dbginfo(...) \
((dbg_level >= DBG_INFO) ? dbgprintf(__VA_ARGS__) : 0)
you can see the comma operator and the ternary operator at work.
C Macros as statement
This is an even more controversial use. Some people say that introducing new statements in the language makes things more confusing and maintainers will have a hard time reading non-standard code.
I claim that maintainers will welcome anything that makes the code clearer, they will have to learn a lot about the code anyway, and using a statement-like macro can greatly help in clarifying the intent of the code.
which one seems clearer to you between:
foreachnode(node,list) {
if (node.info < 7) ...
}
and:
for(node_t node = list; node; node = node->next ) {
if (node.info < 7) ...
}
?
Not to mention that if tomorrow you decide to use a different representation for a list, you only have to change the macro foreachnode()
.
Note that I'm not advocating horrors like this:
#define BEGIN {
#define END }
I'm saying that some well-crafted macros can add expressiveness to your code; as an example, have a look at my previous post on adding exceptions to C where I added the instruction try/catch
for exceptions handling whose syntax fits nicely with the rest of the C code.
try {
// code that throws exceptions
}
catch(ERROR1) {
// code to handle the exception ERROR1
}
catchall {
// code to handle any other exception
}
Crafting statements 1/3
The most common way to create a new statement is to enclose a set of instructions into a do ... while
loop:
#define emit(x) \
do { \
int x_local = x; \
if (status == DIRTY) abort(); \
if (x_local <= 0) than x_local = 255; \
status = CLEAN; \
send_to_peer(x_local); \
} while(0) \
Since the exit condition is always false, the code will be executed only once.
The fact that the macro is exactly a single statement makes it usable in any context (for example as the body of for
loop or as a branch of an if
statement).
This can be a great way to remove boilerplate code (the various checks). Of course for this minimal example, a function could be more appropriate, but it's just to illustrate the point.
Note also that the do { ... } while(0)
trick gives you a way to handle arguments that must be used multiple times: you define local variables to hold the argument values (evaluated exactly once) and use those variables in the code.
Crafting statements 2/3
If you want to craft a statement with a body, you can use an if
statement in your macro:
static volatile int zero = 0;
#define block if (printf("STARTING\n") && zero) ; else
now you can write code like this:
block single_instruction();
block {
// a lot of instructions
}
Of course, this makes sense only if the initialization code is more complex than just printing a message, that's why this trick is not so common as the do ... while(0)
one.
Note that the variable zero
might be needed if your compiler complains that you shouldn't make a test that is always false.
Crafting statements 3/3
A very flexible way to create statements is to use the for
loop. This allows you to execute code at the beginning and at the end of a block of code. For example this macro:
typedef struct {
clock_t start;
int elapsed;
} dbg_clock_t;
#define dbgclock \
for (dbg_clock_t dbg_clock = {.elapsed = -1} ; \
\
(dbg_clock.elapsed < 0) && \
(dbgprintf("START"),dbg_clock.start = clock()); \
\
dbg_clock.elapsed = (int)(clock()-dbg_clock.start) , \
dbgprintf("ELAPSED: %d",dbg_clock.elapsed) \
)
implements a poor man profiler to measure the time spent in a block of code (this is just an example all the usual caveats about clock()
apply here).
// Check if timing is improved
dbgclock {
// Do a lot of staff
}
Note that to hold all the local variable you may need, you may have to define an ad-hoc structure as in the example above.
I devised this use of for
myself but I'm sure someone had used it long before me.
Conclusion
Expressing yourself clearly is the most important thing when writing code (remember the violent psychopath maintainer!).
C
macros can be used to make your code more legible and clear.
Surely, they are sharp tools but, as a C programmer, you are probably already used to handling everything with care.
Post Scriptum
I'm wondering if these types of posts are too long, too confusing, too boring, etc. I would welcome any feedback you may have to make them better!
Top comments (0)