7

I'm working on a school project and the teacher asked us to free all resources at program's termination.

I'm struggling to find a way to write more readable and/or less code to manage that, specifically considering this is made more complex by the fact that different exit points will probably have a different set of resources to free.

The simplest and most messy solution seems to be this(exit points are represented as "some_sys_call" calls that return -1):

char *a = malloc(1);
if(some_sys_call() == -1){
   free(a);
   return -1;
}
//b is needed until the end
char *b = malloc(1);
if(some_sys_call() == -1){
   free(a);
   free(b);
   return -1;
}
//c is needed until the end
char *c = malloc(1);
//a is no longer needed from here on, so it's freed right away
free(a);
if(some_sys_call() == -1){
   free(b);
   free(c);
   return -1;
}
//Exit point when there are no errors
free(b);
free(c);
return 0;

This doesn't seem very appealing for obvious reasons: you need to write a lot of code especially when you have a lot of resources, making the code bloated with frees and less readable. Of course you can't simply write a macro or function that frees all the resources and call it at each exit point, like this:

#define free_all  free(a); \
                  free(b); \
                  free(c);

You could technically define different macros/functions for each set of frees:

#define free_1 free(a); 

#define free_2 free(a); \
               free(b);
...

This would make the "actual" code more readable but would still require you to write a lot of different macros and doesn't seem to me like a good solution either.

Is this considered a common problem? How is this usually handled?

Ernaldo
  • 154
  • 7
  • 2
    https://stackoverflow.com/questions/5677347/is-there-a-better-way-to-do-c-style-error-handling – KamilCuk Jul 12 '23 at 18:06
  • 1
    One of the strongest reasons I can think of to always free resources, even if it's at exit time, is that it keeps the noise away when analyzing a program with _Valgrind_ and/or _LeakSanitizer_ in search for _real_ memory leaks. – Ted Lyngmo Jul 12 '23 at 18:11
  • 1
    Probably that and I guess also to teach us how to write code when you don't have an OS doing the clean up for you? The class is called "operating systems" – Ernaldo Jul 12 '23 at 18:16
  • use `atexit` and in that function perform the cleanups. – t0mm13b Jul 12 '23 at 19:10
  • 2
    If you find this unwieldy, try to write shorter functions and allocate less stuff and there will be less freeing in each function. This is a major reason to keep functions short in C. – user253751 Jul 12 '23 at 19:32
  • 1
    @Ernaldo `free(a); if(some_sys_call() == -1){ free(a);` looks like a double free error. Was that your intent? – chux - Reinstate Monica Jul 13 '23 at 00:59
  • No, it was a mistake. Fixed it, thanks for pointing that out. – Ernaldo Jul 13 '23 at 12:36
  • 'free all resources at program's termination' - that is not guaranteed to be possible with user code, never mind the morass of issues thst will arise with more complex apps:( – Martin James Jul 13 '23 at 17:02
  • 'The class is called "operating systems"' aha! I suspect that the exercise is designed to demonstrate that such an effort is unsustainable during development. Without OS support, effectively terminating a process is impo....'very, very difficult':) – Martin James Jul 13 '23 at 17:13
  • I mean, just consider this: every time you add more code/functionality, you will have to go through a retest of your user cleanup code to make sure it still works:(( – Martin James Jul 13 '23 at 17:15
  • ...and with more complex apps, you will have no idea, on an instantaneous basis, which thread currently owns what buffer pointer... – Martin James Jul 13 '23 at 17:21
  • In summary, I guarantee that attempting to clean up every resource with user code will result in a geometric explosion of effort, bugs and frustration with app complexity:( – Martin James Jul 13 '23 at 17:24
  • Just call your OS 'terminateProcess()' call, you know it makes sense:) – Martin James Jul 13 '23 at 17:26
  • A real world example was AmigaOS. There was no resource tracking, so a crashed process would leave memory allocated. The main reason was that all memory was shared between processes, so ownership of a shared memory segment was unclear. Not secure, but it worked most of the time for single users. There were utilities to try to clean things up, but generally rebooting was just accepted as something you do regularly (e.g. instead of sleep mode - there was no "shutdown", you just hit the power switch, and AmigaOS could boot about as fast as Linux waking and entering a password). – John Bayko Jul 13 '23 at 17:52

5 Answers5

11

This is a well known practice in C, although if it is best or not is disputable

char *a = malloc(1);
char *b = NULL;
char *c = NULL;
int ret = 0;

if(some_sys_call() == -1) {
  goto fail;
}

b = malloc(1);
if(some_sys_call() == -1) {
  goto fail;
}

c = malloc(1);
if(some_sys_call() == -1) {
  goto fail;
}

goto cleanup;

fail:
  ret = -1;
cleanup:
  free(c);
  free(b);
  free(a);

return ret;

The same but a bit shorter (inspired by proxyres /resolver_win8.c:338):

char *a = malloc(1);
char *b = NULL;
char *c = NULL;
int ret = 0;

if(some_sys_call() == -1) {
  goto fail;
}

b = malloc(1);
if(some_sys_call() == -1) {
  goto fail;
}

c = malloc(1);
if(some_sys_call() == -1) {
fail:
  ret = -1;
}

free(c);
free(b);
free(a);

return ret;
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
273K
  • 29,503
  • 10
  • 41
  • 64
  • 5
    This is one of the few acceptable uses of `goto` in C code. – dbush Jul 12 '23 at 18:25
  • Using a macro in place of the goto here would be bad? Something like my free_all – Ernaldo Jul 12 '23 at 19:32
  • @Ernaldo Using a macro is often bad. Imagine you add `char *d`, you would have to update a macro and all macro usages. Whereas you would add above a single line with `free(d)`. A macro is always defined globally, you have to use `#undef` if you need specific `free_all` in different functions. – 273K Jul 12 '23 at 19:46
  • @273K If you were to add some variable, wouldn't you just need to change the macro and not the macro usages? Also to address your second point, you could define multiple free macros like: free_main, free_func1, etc. Is using macros considered bad in general? I usually use them almost as void functions that share scope with the caller. – Ernaldo Jul 12 '23 at 20:32
7

Variation on the @273K good answer.

// Return 0 on success
int foo() {
  // Initialize all with "failed" default values
  char *a = NULL;
  char *b = NULL;
  char *c = NULL;
  int ret = -1;

  a = malloc(1);
  if(a == NULL || some_sys_call() == -1) {
    goto cleanup;
  }

  b = malloc(1);
  if(b == NULL || some_sys_call() == -1) {
    goto cleanup;
  }

  c = malloc(1);
  if(c == NULL || some_sys_call() == -1) {
    goto cleanup;
  }

  // Assign success only if we made it this far.
  ret = 0; // Success!

  cleanup:
  free(c);
  free(b);
  free(a);
  return ret;
}
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
4

There are a few ways to do, but one is to have a function like so:

void free_all(void *a, void *b, void *c )
{
    /* We check to see if the pointer is actually allocated.  You should declare
    ** each pointer in the main module like so:  char *a = NULL, *b = NULL; */
    if(a)
        free(a);
    /*... and repeat for each and every parameter pointer... */
}

Then you can implement in main() like so:

if(some_error)
    free_all(a,b,c);
  • 1
    Agree. And: if you free some resource which is no longer needed -- assign NULL to the pointer just after that, to be sur it is already deallocated. – ReAl Jul 12 '23 at 18:20
  • 9
    You don't need to check `if(a)` because `free(NULL)` is a documented no-op. C18 7.22.3.3 says *If `ptr` is a null pointer, no action occurs.* – Weather Vane Jul 12 '23 at 18:28
  • 2
    greg spears, `if(a) free(a);` simpler as `free(a);`. – chux - Reinstate Monica Jul 13 '23 at 01:09
  • All -- thanks for the illumination that pointers no longer need to be checked before calling free(). Back in the day, I crashed a few apps by trying to free() stuff that wasn't allocated, and now I've built this habit of checking which I can gratefully drop! – greg spears Jul 13 '23 at 19:57
4

While this veers heavily into opinion territory, you might implement a linked list data structure that holds pointers to allocated memory and add them as they are successfully allocated. Then at a failure, you need only walk that list and free each pointer.

If you use a map, you can remove pointers from it if they are freed in the normal course of the program's execution.

Chris
  • 26,361
  • 5
  • 21
  • 42
  • 1
    This seems to be the most versatile and "high level" solution, unless I'm missing something. – Ernaldo Jul 12 '23 at 20:21
  • @Ernaldo, What is missing is that the question was more general than a linked-list. – chux - Reinstate Monica Jul 13 '23 at 01:07
  • @chux-ReinstateMonica The other solutions don't allow you for example to declare pointers to allocated memory inside nested blocks. I think you also need to declare all those pointers specifically at the start of the code for your solution to work. The hash map solution doesn't have these limitations I think. Anyway I noticed my main problem was that my functions were getting too large, if I make them smaller the "goto" solution would probably work best. – Ernaldo Jul 13 '23 at 12:31
4

My general strategy is to separate allocate/free from usage, like this:

int f(int x) {
    X_Buf *x_buf = malloc(x * ...);
    int f_ret = f_with_buf(x, x_buf);
    free(x_buf);

    return f_ret;
}

This also works with other resources, like open/close, fopen/fclose, mmap, and so on. The f_with_buf() function can return normally, or early with errors, and never worry about the need to free, since it didn't do the malloc.

You can chain these together when you need to allocate more than one thing, and there are dependencies:

int f(int x, double y) {
    X_Buf *x_buf = malloc(x * ...);
    int f_ret = f_with_x_buf(x, x_buf, y);
    free(x_buf);

    return f_ret;
}
int f_with_x_buf(x, x_buf, y) {
    int f_ret = f_x(x, x_buf);
    if (OK != f_ret) return f_ret;

    Y_Buf *y_buf = malloc(...);
    y_buf.x_buf = x_buf;

    f_ret = f_y(y, y_buf);
    free(y_buf);

    return f_ret;
}

But not all code has a nice obtain-use-release pattern. In those cases I think the best you can do is put a reference to any resources that need to be cleaned up in a global location, then use a cleanup function, either called explicitly, or using atexit(). Exactly what to keep and how to reference it depends on the app.

You can also kind of follow C++ practice. Although you can't have constructors and destructors, you can have functions that allocate and acquire all resources a struct needs, and another that deallocates them and frees it, e.g struct Y *y_new() and y_del(struct Y *y).

John Bayko
  • 746
  • 4
  • 7