For example, if we allocate objects like this:
void foo(void) {
static const size_t size = 10000000; /* ten million */
int data[size]; /* data is a very large object */
/* do something with data */
}
This is a bad idea for my taste.
I know that, writing code that does not leak is
VERY difficult in C.
Two ideas to cope:
1) object construction:
/*
Resource type whose initialization entails dynamic memory allocation
for and initialization of sub-resources.
*/
struct res
{
struct sub_res1 *res1;
struct sub_res2 *res2;
struct sub_res3 *res3;
};
/*
Initialize a resource object that has already been allocated.
Return -1 for failure and 0 for success. If -1 is returned, the
contents of the object is indeterminate.
*/
int
res_init(struct res *res, int p1, int p2, int p3)
{
res->res1 = res1_construct(p1);
if (0 != res->res1) {
res->res2 = res2_construct(p2);
if (0 != res->res2) {
res->res3 = res3_construct(p3);
if (0 != res->res3) {
/* object complete */
return 0;
}
res2_destruct(res->res2);
}
res1_destruct(res->res1);
}
return -1;
}
/* Uninitialize a successfully initialized object. */
void
res_uninit(struct res *res)
{
res3_destruct(res->res3);
res2_destruct(res->res2);
res1_destruct(res->res1);
}
/* Allocate and initialize an object. */
struct res *
res_construct(int p1, int p2, int p3)
{
struct res *res;
res = malloc(sizeof *res);
if (0 != res) {
if (0 == res_init(res, p1, p2, p3) {
return res;
}
free(res):
}
return 0;
}
/* Uninitialize and free an allocated and initialized object. */
void
res_destruct(struct res *res)
{
res_uninit(res);
free(res);
}
This goes on recursively, that is, the res1_construct(),
res2_construct() and res3_construct() functions called in res_init() all
have the same structure as res_construct() itself. Additionally,
res_construct() can participate in an even-higher level _init() routine.
The _init() functions allow the programmer to define an "outermost"
object with automatic or static storage duration, or to initialize a
structure member object of an already allocated structure. (What is
"outermost" depends on the programmer's situation, so it's useful to
declare all _init() and _uninit() functions separately from _construct()
and _destruct(), and with external linkage.)
The _init() functions can allocate all kinds of system resources, not
just memory (eg. file descriptors to all kinds of files.)
The whole thing mimics the constructor/destructor stuff of C++.
2) Temporary object construction for computation: this is almost
identical to the _init() functions, except that the innermost
success-return is replaced with storing the result and a success
indicator, and with releasing the innermost resource. The unwinding
happens unconditionally here:
/*
Compute some result based on p1, p2, p3. If the computation was
successful, 0 is returned and the result is stored in *result.
Otherwise, -1 is returned.
*/
int
compute(struct result_type *result, int p1, int p2, int p3)
{
int ret;
struct sub_res1 tmp1;
ret = -1;
if (-1 != res1_init(&tmp1, p1)) {
struct sub_res2 tmp2;
if (-1 != res2_init(&tmp2, p2)) {
struct sub_res3 tmp3;
if (-1 != res3_init(&tmp3, p3)) {
/* All objects present, do computation. */
*result = ...;
ret = 0;
res3_uninit(&tmp3);
}
res2_uninit(&tmp2);
}
res1_uninit(&tmp1);
}
return ret;
}
Ideas 1 and 2 can be recursively combined, too; for example, some
computation may be necessary to initialize an object, and the compute()
function above already relies on idea 1 (object initialization). No such
call-tree leaks (unless I botched up the code above, but you get the
idea).
Note that the _uninit() and _destruct() functions described above are
unable to signal errors. If such an _uninit() calls eg. fclose(), that's
lossy, because fclose() might try to flush output and it could fail.
This is only relevant in the compute() case, not the object construction
case, because in the latter case, we will signal an error anyway back to
the caller if we're on the error path.
I can name two solutions to this:
a) Make the _uninit() functions return a success/error value too, and
when walking towards the exit in compute(), have any failed _uninit()
reset "ret" to -1 and destroy (release) *result.
b) Make fflush() part of the computation, or more generally, make sure
that once we set "ret = 0", nothing can go wrong within reason.
.... I cheated a little, because even compute() is a sort of object
initialization -- that of "*result".
These "patterns" cannot be used indiscriminately. The idea to take away
is the staircase-like embedding of "if" statements. (Many people hate it
with a passion, because it introduces a lot of basic blocks and
increases "cyclomatic complexity". IMHO with a reasonable resolution
(and consequently, depth) of _init() / compute() functions, things stay
manageable. One benefit of this approach appears to be that you never
have to write O(n^2) pieces pf _uninit() calls in error handling
sections.)
Or something like that.
lacos