C does provide a way to place code syntactically before other code that will execute first: the for
block. Remember that clause 3 of a for
structure can contain an arbitrary expression, and always runs after the execution of the main block.
You can therefore create a macro that makes a predetermined call after a given chunk of following code by wrapping a for
block in a macro:
#define M_GEN_DONE_FLAG() _done_ ## __LINE__
#define M_AROUND_BLOCK2(FLAG, DECL, BEFORE, AFTER) \
for (int FLAG = (BEFORE, 0); !FLAG; ) \
for (DECL; !FLAG; FLAG = (AFTER, 1))
#define M_AROUND_BLOCK(DECL, BEFORE, AFTER) M_AROUND_BLOCK2(M_GEN_DONE_FLAG(), DECL, BEFORE, AFTER)
#define M_CLEANUP_VAR(DECL, CLEANUP_CALL) M_AROUND_BLOCK(DECL, (void)0, CLEANUP_CALL)
...and you can use it like this:
#include <stdio.h>
struct proc;
typedef struct proc * proc_t;
proc_t proc_find(int);
void proc_rele(proc_t);
void fun(int mypid) {
M_CLEANUP_VAR (proc_t myproc = proc_find(mypid), proc_rele(myproc))
{
printf("%p\n", &myproc); // just to prove it's in scope
}
}
The trick here is that a for
block accepts a following statement, but if we don't actually put that statement in the macro definition, we can follow the macro invocation with a normal code block and it will "magically" belong to our new scoped-control-structure syntax, simply by virtue of following the expanded for
.
Any optimizer worth using will remove the loop flag at its lowest optimization settings. Note that name clashing with the flag is not a huge concern (i.e. you don't really need a gensym
for this) because the flag is scoped to the loop body, and any nested loops will safely hide it if they use the same flag name.
The bonus here is that the scope of the variable to cleanup is restricted (it cannot be used outside of the compound immediately following its declaration) and visually explicit (because of said compound).
Pros:
- this is standard C with no extensions
- the control flow is straightforward
- it's actually (somehow) less verbose than
__attribute__ __cleanup__
Cons:
- it doesn't provide "full" RAII (i.e. won't protect against
goto
or C++ exceptions: __cleanup__
is usually implemented with C++ machinery under the hood so it's more complete). More seriously, it doesn't protect against early return
(thanks @Voo). (You can at least guard against a misplaced break
- if you want to - by adding a third line, switch (0) default:
to the end of M_AROUND_BLOCK2
.)
- not everyone agrees with syntax-extending macros (but consider that you are extending C's semantics here, so...)