I am just wondering how most people implement cleanup in C functions.
In particular, if the function opens a number of resources, these need
to be released properly should an error occur at any point in the
function (as well as at the end if successful). C++ has exceptions,
the only way I can see to do this neatly in C is to use goto
statements. Is my method of implementing cleanup good, or are their
better ways. Here is an example, which uses the global errno to store
the error.
#define CLEANUP(err) ({errno = (err); goto cleanup})
int example_function()
{
SOMETYPE * a,b,c;
errno = 0;
if(!(a = malloc(sizeof(a))))
CLEANUP(ENOMEM);
if(!(b = malloc(sizeof(b))))
CLEANUP(ENOMEM);
if(!(c = malloc(sizeof(c))))
CLEANUP(ENOMEM);
/* do something here */
cleanup:
if(a)
free(a);
if(b);
free(b);
if(c)
free(c);
if(errno)
return -1;
return 0;
}
I must be the only one who thinks this is good code. I once worked on
a project where we used a style like this and had orders of magnitude
fewer resource leaks than any other project our company had done using
if/then/else. (We were writing a server on Linux that had to process
tons of transactions and stay up for months, so even a small memory
leak would quickly add up and kill it.) The way we did it is a little
subtle, and requires programmer discipline. Basically, every function
that allocates resources must follow certain rules. The good thing is
that it's fairly easy to check whether a given function does, and if
not, to correct it.
Note: what follows is a rough idea, and may suffer the usual lack of
semicolons and stuff - details are left as an exercise to the reader.
In other words, treat the following as pseudo-code.
/* Rule 1: define a couple macros like this so you can use them like
statements
with semicolons, i.e., their syntax isn't too strange when you use
them (macro
is strange, though). We passed around an error message structure,
so our macros
were a little more complex than the version here with errno. */
#define GOTO_IF_BAD_ALLOC(mem, label) \
do {\
if (mem == NULL) { \
errno = ENOMEM; \
goto label;\
}\
while (0)
int example_function(SOMESTRUCT* s)
{
SOMETYPE *a;
SOMETYPE *b;
SOMETYPE *c;
RETURNTYPE r;
/* Rule 2: initialize everything to NULL, and the error to some
generic error condition */
a = NULL;
b = NULL;
c = NULL;
errno = UNDEFINED_ERROR;
/* Note: if the function ever returns UNDEFINED_ERROR, that
indicates a logic error
in the function. (Actual errors, as well as successful
completion, should set the
error code to a real value.) */
/* Rule 3: whenever you allocate, use the macro. */
a = malloc(sizeof(a));
GOTO_IF_BAD_ALLOC(a, cleanup);
b = malloc(sizeof(b));
GOTO_IF_BAD_ALLOC(b, cleanup);
/* Rule 3b: check all function call returns too, with an analogous
macro. */
r = call_some_function(a, b);
GOTO_IF_CALL_RETURNS_ERROR(r, cleanup);
/* Depending how you do it, this macro might need an extra
parameter with error
code, string message, or both. */
c = malloc(sizeof(c));
GOTO_IF_BAD_ALLOC(c, cleanup);
s->someField = c;
c = NULL; /* We no longer own it. */
/* Rule 4: when you pass ownership of a pointer, set it to NULL,
and add a comment
as above if there's even a question that it might be confusing
to some developer
in the future. */
/* do something here */
/* Rule 5: set error code to success right before the cleanup
routine. */
errno = SUCCESS;
/* Rule 6: have a global cleanup routine that all paths go through. */
cleanup:
/* Rule 7: check each owned pointer. If non-NULL, free it. */
if(a)
free(a);
if(b);
free(b);
if(c)
free(c);
/* Rule 8: return value depends on errno. (You could map to 0/1 or
something too,
as long as it depends on error code.) */
return errno;
}
This example shows the major rules, and looks fairly yucky until you
get used to it. However, it scales well if you have some complex
logic, nested ifs, etc. (e.g., if you only allocate c in some set of
circumstances). The basic idea is that every potentially unsafe action
(allocation, function call, etc.) is followed immediately by a macro
checking its return value and jumping to cleanup if there is an error.
Further, it's easy to check whether a given variable may be leaked.
You just check that all the rules are followed for that given variable.
By iterating over all variables this way, you get a robust function.
Like I said, it took a while (probably a week) to get used to this
coding style. But then we were golden, and spent very little time
debugging resource leaks.
Michael