Hacker News new | past | comments | ask | show | jobs | submit login

I never understood this argument. Without RAII you can easily get screwed by resource leaks without goto when returning early. In this regard, using goto is expedient. How do C programmers avoid this problem without goto?

    bool function_with_cleanup(void) {
        int *buffer1 = NULL;
        int *buffer2 = NULL;
        FILE *file = NULL;
        bool success = false;
        
        // Allocate first resource
        buffer1 = malloc(sizeof(int) * 100);
        if (!buffer1) {
            goto cleanup;  // Error, jump to cleanup
        }
        
        // Allocate second resource
        buffer2 = malloc(sizeof(int) * 200);
        if (!buffer2) {
            goto cleanup;  // Error, jump to cleanup
        }
        
        // Open a file
        file = fopen("data.txt", "r");
        if (!file) {
            goto cleanup;  // Error, jump to cleanup
        }
        
        // Do work with all resources...
        
        success = true;  // Only set to true if everything succeeded
        
    cleanup:
        // Free resources in reverse order of acquisition
        if (file) fclose(file);
        free(buffer2);  // free() is safe on NULL pointers
        free(buffer1);
        
        return success;
    }





Attributes, mostly. Which have become so common that defer is very likely to be in the next C standard. [0]

    bool function_with_cleanup(void) {
        // Allocate first resource
        int* buffer1 __attribute__((__cleanup__(free))) =  malloc(sizeof(int) * 100);
        if(!buffer1) { return false; }

        // Allocate second resource
        int* buffer2 __attribute__((__cleanup__(free))) = malloc(sizeof(int) * 200);
        if(!buffer2) { return false; }

        // Open a file
        FILE* file __attribute__((__cleanup__(fclose))) = fopen("data.txt", "r");
        if (!file) { return false; }

        return true;
    }

[0] https://thephd.dev/c2y-the-defer-technical-specification-its...

Nested ifs are my preference:

  bool function_with_cleanup(void) {
      int *buffer1 = NULL;
      int *buffer2 = NULL;
      FILE *file = NULL;
      bool success = false;
  
      // Allocate first resource
      buffer1 = malloc(sizeof(int) * 100);
      if (buffer1) {
          // Allocate second resource
          buffer2 = malloc(sizeof(int) * 200);
          if (buffer2) {
              // Open a file
              file = fopen("data.txt", "r");
              if (file) {
                  // Do work with all resources...
                  fclose(file);
                  success = true;  // Only set to true if everything succeeded
              }
              free(buffer2);
          }
          free(buffer1);
      }
  
      return success;
  }
Much shorter and more straightforward.

One-time loops with break also work if you're not doing the resource allocation in another loop:

  bool function_with_cleanup(void) {
      int *buffer1 = NULL;
      int *buffer2 = NULL;
      FILE *file = NULL;
      bool success = false;
  
      do { // One-time loop to break out of on error
          // Allocate first resource
          buffer1 = malloc(sizeof(int) * 100);
          if (!buffer1) {
              break;  // Error, jump to cleanup
          }
  
          // Allocate second resource
          buffer2 = malloc(sizeof(int) * 200);
          if (!buffer2) {
              break;  // Error, jump to cleanup
          }
  
          // Open a file
          file = fopen("data.txt", "r");
          if (!file) {
              break;  // Error, jump to cleanup
          }
  
          // Do work with all resources...
  
          success = true;  // Only set to true if everything succeeded
      } while(false);
  
      // Free resources in reverse order of acquisition
      if (file) fclose(file);
      free(buffer2);  // free() is safe on NULL pointers
      free(buffer1);
  
      return success;
  }
Still simpler to follow than goto IMHO. Both these patterns work in other languages without goto too, e.g. Python.

Nested if, aside from being awful, doesn't scale.

And break is extremely thin sugar on top of go-to.


Open a scope when you check resource acquisition passed, rather than the opposite (jump to the end of the function if it failed).

It can get quite hilly, which doesn't look great. It does have the advantage that each resource is explicitly only valid in a visible scope, and there's a marker at the end to denote the valid region of the resource is ending.

EDIT: you mentioned early return, this style forbids early return (at least, any early return after the first resource acquisition)


In this example couldn’t the go to cleanup instead be return cleanup_func where the same cleanup code was executed?

Maybe that is exactly the problem, stop using a language designed in 1970's that ignored on purpose the ecosystem outside Bell Labs, unless where it is unavoidable.

And in such case, the C compiler doesn't have a limit to write functions and better modularize their implementations.

    bool function_with_cleanup(void) {
        int *buffer1 = NULL;
        int *buffer2 = NULL;
        FILE *file = NULL;        
        // Allocate first resource
        buffer1 = malloc(sizeof(int) * 100);
        if (!buffer1) {
            return false;
        }
        
        // Allocate second resource
        buffer2 = malloc(sizeof(int) * 200);
        if (!buffer2) {
            free(buffer1);
            return false;
        }
        
        // Open a file
        file = fopen("data.txt", "r");
        if (!file) {
            free(buffer1);
            free(buffer1);
            return false;
        }
        
        // Do work with all resources...
        fclose(file);
        free(buffer1);
        free(buffer1);

        return true;
    }
Ah, but all those free() calls get tedious can be forgotten and mistyped

    bool function_with_cleanup(void) {
        int *buffer1 = NULL;
        int *buffer2 = NULL;
        FILE *file = NULL;        
        // Allocate first resource
        buffer1 = arena_alloc(&current_arena, sizeof(int) * 100);
        if (!buffer1) {
            return false;
        }
        
        // Allocate second resource
        buffer2 = arena_alloc(&current_arena, sizeof(int) * 200);
        if (!buffer2) {
            arena_reset(&current_arena); 
            return false;
        }
        
        // Open a file
        file = fopen("data.txt", "r");
        if (!file) {
            arena_reset(&current_arena);
            return false;
        }
        
        // Do work with all resources...
        fclose(file);
        arena_reset(&current_arena);
        return true;
     }
Can still be improved with a mix of macros and varargs functions.

Or if using language extensions is a thing, the various ways to do defer in C.


You understand go doesn't have exceptions right?

You understand that I wrote C code, and in what concerns Go, panic/recover are exceptions that don't want to assume themselves as such?

You can't handle a panic, it's the whole point.

In C, you mean?

You clearly can in Go. How would you deal with exceptions otherwise? You can even get silly with it:

    func main() {
        try(func() {
            throw("Oops")
        }).catch(func(e any) {
            fmt.Println("Caught:", e)
        })
    }
https://go.dev/play/p/Y1-w9xUXIcO



Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: