40

Someone will probably say something about exceptions... but in C, what are other ways to do the following cleanly/clearly and without repeating so much code?

if (Do1()) { printf("Failed 1"); return 1; }
if (Do2()) { Undo1(); printf("Failed 2"); return 2; }
if (Do3()) { Undo2(); Undo1(); printf("Failed 3"); return 3; }
if (Do4()) { Undo3(); Undo2(); Undo1(); printf("Failed 4"); return 4; }
if (Do5()) { Undo4(); Undo3(); Undo2(); Undo1(); printf("Failed 5"); return 5; }
Etc...

This might be one case for using gotos. Or maybe multiple inner functions...

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
dargaud
  • 2,431
  • 2
  • 26
  • 39
  • 2
    Just to point out with regard to my previous comment: in these cases, it is generally the case that these cleanup actions need to be also performed when there are no early abortions (hence the mention of free/fclose); that makes the structure with goto & labels fairly straightforward and easy to read. This may not be the case that you are thinking of. – 9769953 Nov 23 '18 at 10:27
  • 2
    *'exceptions'* - no, but [RAII](https://en.cppreference.com/w/cpp/language/raii); still, that's C++... – Aconcagua Nov 23 '18 at 10:59
  • 3
    I believe you need to clarify the types of the functions, as we are getting all kinds of mixed answers. Are they the same type, or are all functions of different types? – Lundin Nov 23 '18 at 13:45
  • 1
    @Lundin It is clear the code is an *example* of the code patterns that appear in many C functions, typically those that allocate resources. Precisely because the types do not matter here, they are not specified. It is pseudocode, if you want. – Acorn Nov 23 '18 at 13:53
  • @Acorn Are you saying that the question should be closed as too broad? Because the best ways to write pseudo code wouldn't really be on-topic. – Lundin Nov 23 '18 at 14:10
  • 6
    @Lundin No, I haven't said anything like it. And no, pseudo-code can be perfectly on topic. – Acorn Nov 23 '18 at 14:26
  • 4
    @9769953 - I'd say the problem wasn't `goto fail;` so much as avoiding curly braces. And it's not like it's a new sort of bug. The offending line didn't have to be a `goto`, but could just as easily have been a `exit(EXIT_FAILURE)`. Still the same bug, despite the different manifestation. – StoryTeller - Unslander Monica Nov 23 '18 at 15:18
  • 1
    Judging by the number of different answers, the actual answer seems to be "Do whatever you want". – Not a real meerkat Nov 23 '18 at 18:53
  • 1
    Its funny that none of the answers are as clean and concise as the question. – James Nov 28 '18 at 15:05

15 Answers15

49

Yes, it's quite common to use goto in such cases to avoid repeating yourself.

An example:

int hello() {
  int result;

  if (Do1()) { result = 1; goto err_one; }
  if (Do2()) { result = 2; goto err_two; }
  if (Do3()) { result = 3; goto err_three; }
  if (Do4()) { result = 4; goto err_four; }
  if (Do5()) { result = 5; goto err_five; }

  // Assuming you'd like to return 0 on success.
  return 0;

err_five:
  Undo4();
err_four:
  Undo3();
err_three:
  Undo2();
err_two:
  Undo1();
err_one:
  printf("Failed %i", result); 
  return result;
}

As always you probably also want to keep your functions small and batch together the operations in a meaningful manner to avoid a large "undo-code".

likle
  • 1,717
  • 8
  • 10
  • 1
    Note: `return 0` may or may not be needed, depending on what the function is supposed to do. – Acorn Nov 23 '18 at 10:55
  • 1
    @Acorn Without, code wouldn't be equivalent to code presented in question, which undoes only on error... – Aconcagua Nov 23 '18 at 11:37
  • 1
    @Aconcagua The code in the question does not show what should be done in the correct case. – Acorn Nov 23 '18 at 11:42
  • This is just a different flavour of code repetition, it doesn't solve anything, just moves the repetition elsewhere. – Lundin Nov 23 '18 at 12:05
  • 1
    @Lundin Just to clarify what I meant in this answer - You don't repeat the whole undo-stack in each error case. However, I think it would be really nice if you could elaborate a bit more on the repetition shift and why the function design in the OP's example is bad in your own answer. – likle Nov 23 '18 at 12:13
  • 1
    @likle Whenever you find your self using a postfix `_1` or `_one` to your identifiers, that's the first sign of code repetition. Whenever you change something, you must change the whole code. Makes it harder to maintain and also easy to slip. Avoiding code repetition = using arrays. – Lundin Nov 23 '18 at 12:24
  • 3
    @Lundin I agree. This answer assumes that the Do and Undo functions might have different signatures in reality which is probably more common in practice. – likle Nov 23 '18 at 12:34
  • 11
    @Lundin There is no code repetition here. Please, don't misunderstand generic examples with concrete ones. – Acorn Nov 23 '18 at 13:49
  • 1
    It goes like this: during maintenance I need to add "Do 3.1". First I change the code in one place, below `Do3`: `if (Do3_1()) { result = 3; goto err_three_one; }`. Perfectly reasonable. Then I change the code in another place, below err_three: `err_three_one: Undo3_1();`. Perfectly reasonable. Oh... err... wait, should the label be above `err_three` or after it? Oh and wait, now I have changed the behavior of all the other 5 cases because everything unrelated is tightly coupled. [Enter spaghetti maintenance nightmare]. – Lundin Nov 23 '18 at 15:18
  • 11
    @Lundin It seems you don't understand how this approach works. If you have to add another case, there is *no* need to change *anything* else. That is the entire point. – Acorn Nov 23 '18 at 16:35
  • Interestingly, an equivalent solution (but more general, and documented) I wrote at the same time as this one was downvoted to oblivion. :) https://stackoverflow.com/a/53445054/9305398 – Acorn Nov 24 '18 at 14:40
  • @Acorn I agree, your answer is good as well.. I just gave you my vote. – likle Nov 24 '18 at 14:48
  • @Acorn You are assuming that the 5 different tasks are always coupled, and that the undos are always executed in sequence. If that's not the case, which _we can't know based on the information in the question_, then this code will break during maintenance. – Lundin Nov 25 '18 at 15:14
18

This might be one case for using gotos.

Sure, let's try that. Here's a possible implementation:

#include "stdio.h"
int main(int argc, char **argv) {
    int errorCode = 0;
    if (Do1()) { errorCode = 1; goto undo_0; }
    if (Do2()) { errorCode = 2; goto undo_1; }
    if (Do3()) { errorCode = 3; goto undo_2; }
    if (Do4()) { errorCode = 4; goto undo_3; }
    if (Do5()) { errorCode = 5; goto undo_4; }

undo_5: Undo5();    /* deliberate fallthrough */
undo_4: Undo4();
undo_3: Undo3();
undo_2: Undo2();
undo_1: Undo1();
undo_0: /* nothing to undo in this case */

    if (errorCode != 0) {
        printf("Failed %d\n", errorCode);
    }
    return errorCode;
}
Blaze
  • 16,736
  • 2
  • 25
  • 44
  • please explain -1, whiich is mistake? I liked this approach. – Grijesh Chauhan Nov 23 '18 at 10:38
  • 2
    The -1 is probablky from someone who considers `goto` as absolutely evil, which it is not as long as it's abused. – Jabberwocky Nov 23 '18 at 10:41
  • No, it is because of the weird usage of `errorCode`. Normally, the `printf` is anything that must be done case-by-case (e.g. custom error message), and cannot be done at the end generically. – Acorn Nov 23 '18 at 10:48
  • @Acorn Either - or no `printf`` at all, leaving error logging to the calling function (the latter to be preferred in library functions). But I don't think this *minor* issue disqualifies the answer that much that a downvote (in addition to comments) is justified... – Aconcagua Nov 23 '18 at 11:05
  • I'd have used `0` instead of `-1`. – alk Nov 23 '18 at 11:07
  • 1
    @Aconcagua It is rare to be able to factorize the `printf`, and even if you can, it is not typically done. In a "generic" example like this, you shouldn't write it like that, because beginners will think it is the common way of doing it, in my opinion. Also, there is the point about the initialization to `-1` and also not talking about cases where you may have to return early (i.e. before the `Undo`s). Finally, better answers are currently lower than this one. – Acorn Nov 23 '18 at 11:14
  • Thank you for the feedback, everyone. OP specifically requested for "ways to do the following cleanly/clearly and without repeating so much code", so I opted for a solution that achieves exactly that without making too many assumptions about his code that would increase code duplication. – Blaze Nov 23 '18 at 11:27
  • @Blaze I would say OP is actually asking for the generic case/solution (as in the title: "Clean ways to do multiple undo in C?"). The example code is simply a way to be able to discuss the solutions. – Acorn Nov 23 '18 at 11:35
  • This is code repetition. The OP wished to avoid it. – Lundin Nov 23 '18 at 12:15
  • 2
    @Lundin It isn't. – Acorn Nov 23 '18 at 13:41
  • 1
    @Acorn The error behavior of every single error code is tightly coupled with the error behavior of every other error code. So if you add something in the middle during maintenance, it all comes crashing down. – Lundin Nov 23 '18 at 15:26
  • 3
    @Lundin It isn't tied, at all. You are making assumptions, and they do not even correspond to actual code out there. – Acorn Nov 23 '18 at 16:27
  • 1
    Just one single `return errorCode;` *after* the if would be equivalent, but nicer... – Aconcagua Nov 24 '18 at 08:42
14

If you have the same signature for your function you can do something like this:

bool Do1(void) { printf("function %s\n", __func__); return true;}
bool Do2(void) { printf("function %s\n", __func__); return true;}
bool Do3(void) { printf("function %s\n", __func__); return false;}
bool Do4(void) { printf("function %s\n", __func__); return true;}
bool Do5(void) { printf("function %s\n", __func__); return true;}

void Undo1(void) { printf("function %s\n", __func__);}
void Undo2(void) { printf("function %s\n", __func__);}
void Undo3(void) { printf("function %s\n", __func__);}
void Undo4(void) { printf("function %s\n", __func__);}
void Undo5(void) { printf("function %s\n", __func__);}


typedef struct action {
    bool (*Do)(void);
    void (*Undo)(void);
} action_s;


int main(void)
{
    action_s actions[] = {{Do1, Undo1},
                          {Do2, Undo2},
                          {Do3, Undo3},
                          {Do4, Undo4},
                          {Do5, Undo5},
                          {NULL, NULL}};

    for (size_t i = 0; actions[i].Do; ++i) {
        if (!actions[i].Do()) {
            printf("Failed %zu.\n", i + 1);
            for (int j = i - 1; j >= 0; --j) {
                actions[j].Undo();
            }
            return (i);
        }
    }

    return (0);
}

You can change the return of one of Do functions to see how it react :)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tom's
  • 2,448
  • 10
  • 22
8

For completeness a bit of obfuscation:

int foo(void)
{
  int rc;

  if (0
    || (rc = 1, do1()) 
    || (rc = 2, do2()) 
    || (rc = 3, do3()) 
    || (rc = 4, do4()) 
    || (rc = 5, do5())
    || (rc = 0)
  ) 
  {
    /* More or less stolen from Chris' answer: 
       https://stackoverflow.com/a/53444967/694576) */
    switch(rc - 1)
    {
      case 5: /* Not needed for this example, but left in in case we'd add do6() ... */
        undo5();

      case 4:
        undo4();

      case 3:
        undo3();

      case 2:
        undo2();

      case 1:
        undo1();

      default:
        break;
    }
  }

  return rc;
}
alk
  • 69,737
  • 10
  • 105
  • 255
  • 5
    Please, stop this madness. – Acorn Nov 23 '18 at 11:29
  • 3
    @Acorn: Why? Nice example for how to use the comma-operator ... ;-) – alk Nov 23 '18 at 11:34
  • 1
    @Acorn: Also nicely symmetrical and compact, perfect to auto-generate. – alk Nov 23 '18 at 11:36
  • 2
    The other solutions are just as easy to auto-generate (if you are talking about code generation). – Acorn Nov 23 '18 at 11:44
  • 14
    Now all we are missing is a version with Duff's device and we'll be ready to submit this thread to [IOCCC](https://www.ioccc.org/) :) – Lundin Nov 23 '18 at 12:20
  • @alk Further, the solution is not general: `doN()` are here placeholders for actual statements (plural). – Acorn Nov 23 '18 at 14:04
  • @Acorn: "*are here placeholders*" Are they? I read nothing like this from the OP. – alk Nov 23 '18 at 14:06
  • @alk Nothing states the opposite, either. And, considering the rest of the question (including the *title*), it is obvious OP is talking about code patterns, not actual functions. – Acorn Nov 23 '18 at 14:07
5

Use goto to manage cleanup in C.

For instance, check the Linux kernel coding style:

The rationale for using gotos is:

  • unconditional statements are easier to understand and follow nesting is reduced
  • errors by not updating individual exit points when making modifications are prevented
  • saves the compiler work to optimize redundant code away ;)

Example:

int fun(int a)
{
    int result = 0;
    char *buffer;

    buffer = kmalloc(SIZE, GFP_KERNEL);
    if (!buffer)
        return -ENOMEM;

    if (condition1) {
        while (loop1) {
            ...
        }
        result = 1;
        goto out_free_buffer;
    }

    ...

out_free_buffer:
    kfree(buffer);
    return result;
}

In your particular case, it could look like:

int f(...)
{
    int ret;

    if (Do1()) {
        printf("Failed 1");
        ret = 1;
        goto undo1;
    }

    ...

    if (Do5()) {
        printf("Failed 5");
        ret = 5;
        goto undo5;
    }

    // all good, return here if you need to keep the resources
    // (or not, if you want them deallocated; in that case initialize `ret`)
    return 0;

undo5:
    Undo4();
...
undo1:
    return ret;
}
Acorn
  • 24,970
  • 5
  • 40
  • 69
  • 2
    This is somewhat acceptable use of goto - it is a pattern from BASIC known as "on error goto". I wish people wouldn't stop thinking there, but think one step further still. The better alternative to "on error goto" is to use a wrapper function and from the inner function `return code;` upon error. Leave resource allocation and clean-up to the outer wrapper. Thus separate resource allocation and algorithm in 2 different functions. Much cleaner design, easier to read and no goto debate. In general, I would recommend staying away from "the Linux kernel coding style" document. – Lundin Nov 23 '18 at 12:13
  • @Lundin Not sure, are you now referring Tom's [function pointer solution](https://stackoverflow.com/a/53445947/1312382) (your description sounds somehow different to me...)? If not, how would then these wrapper functions look like? Cannot think of anything better than `int callDo2(void) { if(do2()) { undo1(); return 2; } return callDo3(); }` at the moment, but cannot imagine either that you really meant *such* ones... – Aconcagua Nov 23 '18 at 13:02
  • 1
    Basically replace all your goto with return, then in the outer wrapper function call "undo" based on what the function returned. – Lundin Nov 23 '18 at 13:39
  • @Lundin Please show an example, because it does not sound like a good idea at all. – Acorn Nov 23 '18 at 13:41
3

There are probably many ways to do this, but one idea is since you won't call one function unless the preceeding one succeeded, you could chain your function calls using else if like this. And using a variable to track where it fails you can use a switch statement to roll back easily too.

int ret=0;
if(Do1()) {
    ret=1;
} else if(Do2()) {
    ret=2;
} else if(Do3()) {
    ret=3;
} else if(Do4()) {
    ret=4;
} else if(Do5()) {
    ret=5;
}

switch(ret) {   
    case 5:  
        Undo4();
    case 4:  
        Undo3();
    case 3:  
        Undo2();
    case 2:  
        Undo1();
    case 1:
        printf("Failed %d\n",ret);
    break; 
}
return ret;
Chris Turner
  • 8,082
  • 1
  • 14
  • 18
  • 8
    Don't do this. The code is harder to read and doing more branches compared to simple `goto`. – Acorn Nov 23 '18 at 10:39
  • 4
    Also, note it is wrong: if `Do5()` fails, we don't want to run `Undo5()` (typically). – Acorn Nov 23 '18 at 10:57
  • 1
    `switch(ret)` should be `switch(ret-1)`. Also an (emtpy) `default` case would be nice. All in all I like this approach. – alk Nov 23 '18 at 11:04
  • @alk Writing the actual correct values would be better -- if you are keen on using this solution, which you should not ;) As for the `default` case, what for? – Acorn Nov 23 '18 at 11:20
  • 6
    The lengths people will go to avoid uncoditional jump which was basically kept in C language exactly for such cases because it makes it more readable than arrow antipattern or switch/if ladder is amazing... Take my downvote. – Purple Ice Nov 23 '18 at 11:54
  • @Acorn "harder to read" is entirely subjective - the logic flow is clear in my answer as you can easily see that if one of the `if` statements is triggered, the subsequent ones aren't. – Chris Turner Nov 23 '18 at 15:09
  • @ChrisTurner It is objectively more complex, see cyclomatic complexity. – Acorn Nov 23 '18 at 16:05
  • @Acorn less complex - using `else if` ensures there is only one path the code can take. having a series of `if`s means that any number of paths could be taken. – Chris Turner Nov 23 '18 at 16:31
  • @ChrisTurner No. Each `if` can only be taken once, and if one is taken, the others aren't. It is exactly equivalent to using `else if` (while the undo part is more complex than the `goto` equivalent). – Acorn Nov 23 '18 at 16:32
  • 1
    @Acorn that isn't how `if` works though; having to check what if the `if` does to see if it circumvents the execution flow using a `goto` is a lot more work than seeing that the next conditional is an `else if` – Chris Turner Nov 23 '18 at 16:50
  • @ChrisTurner I am pretty sure how `if` works. The fact remains: this solution has more complex control flow than the other. If you want to argue that `else` is easier to see than `return` or `goto` by your eyes, fine; but then I am pretty unsure how you find your bottom `if` and `switch` and `case`s easy to read. – Acorn Nov 23 '18 at 17:00
  • @Acorn the bottom `if` isn't needed as the `printf` could go into the last `case`... the `switch` and `case`s are no different to the series of `labels` in the `goto` answers. – Chris Turner Nov 23 '18 at 17:11
  • @ChrisTurner Even in your edited version, the flow is not as trivial as it may seem to you. You are still picking branches that you already know should be picking (because you already did the branching in the upper part). In fact, not even modern compilers have it easy: the compiler has to realize the `ret` values matches all the ones in the `switch` and disentangle the mess. In fact, only gcc seems to avoid generating extra branches or a `switch` table. – Acorn Nov 23 '18 at 17:35
2

Yes, as explained by other answers, using goto for error-handling is often appropriate in C.

That said, if possible, you probably should make your cleanup code safe to call even if the corresponding action was never performed. For example, instead of:

void foo()
{
    int result;
    int* p = malloc(...);
    if (p == NULL) { result = 1; goto err1; }
   
    int* p2 = malloc(...);
    if (p2 == NULL) { result = 2; goto err2; }

    int* p3 = malloc(...);
    if (p3 == NULL) { result = 3; goto err3; }

    // Do something with p, p2, and p3.
    bar(p, p2, p3);

    // Maybe bar() saved references to p and p2, but we don't need
    // p3 anymore.
    free(p3);    

    return 0;

err3:    
    free(p2);
err2:
    free(p);
err1:
    return result;
}

I'd advocate:

void foo()
{
    int result = -1; // Or some generic error code for unknown errors.

    int* p = NULL;
    int* p2 = NULL;
    int* p3 = NULL;

    p = malloc(...);
    if (p == NULL) { result = 1; goto exit; }
   
    p2 = malloc(...);
    if (p2 == NULL) { result = 2; goto exit; }

    p3 = malloc(...);
    if (p3 == NULL) { result = 3; goto exit; }

    // Do something with p, p2, and p3.
    bar(p, p2, p3);

    // Set success *only* on the successful path.
    result = 0;

exit:
    // free(NULL) is a no-op, so this is safe even if p3 was never allocated.
    free(p3);

    if (result != 0)
    {
        free(p2);
        free(p);
    }
    return result;
}

It's slightly less efficient since it requires initializing variables to NULL, but it's more maintainable since you don't need extra labels. There's less stuff to get wrong when making changes to the code. Also, if there's cleanup code that you need on both success and failure paths, you can avoid code duplication.

jamesdlin
  • 81,374
  • 13
  • 159
  • 204
  • It is worse because it needs extra branches or functions that ignore wrong arguments like `free`. – Acorn Jun 11 '21 at 10:29
  • `if (result != 0)` can be removed. – Acorn Jun 11 '21 at 10:30
  • @Acorn `free(NULL)` would be called only along failure paths, which presumably should be uncommon. Making code more complicated to optimize for rare cases seems like a wrong trade-off. Regarding `if (result != 0)`, it's necessary to be equivalent to the (admittedly contrived) first example with multiple labels. – jamesdlin Jun 11 '21 at 11:34
  • Extra branches means added complexity. Performance is only one of the problems. – Acorn Jun 11 '21 at 13:33
  • No, `if (result != 0)` is not necessary. – Acorn Jun 11 '21 at 13:33
  • The first example also calls `free` when is not needed. – Acorn Jun 11 '21 at 13:34
  • As I alluded to, both examples were trying to demonstrate a case where some allocations are preserved along the success path, hence the `if (result != 0)` check. – jamesdlin Jun 11 '21 at 19:36
  • I also don't understand what you're talking about regarding extra branches. Both versions have `if` branches with a `goto` for each failure point. The second version has fewer *labels*, which is *less complex* and easier to maintain. – jamesdlin Jun 11 '21 at 19:39
  • Extra branches are needed in your approach to skip parts of the error handling (`if (result != 0)`) as well as to ignore release operations that should be noop (`free` does it internally). With an optimal `goto` strategy or with RAII all those branches can go away. – Acorn Jun 12 '21 at 08:05
0

I typically approach this kind of problem by nesting the conditionals:

int rval = 1;
if (!Do1()) {
    if (!Do2()) {
        if (!Do3()) {
            if (!Do4()) {
                if (!Do5()) {
                    return 0;
                    // or "goto succeeded", or ...;
                } else {
                    printf("Failed 5");
                    rval = 5;
                }
                Undo4();
            } else {
                printf("Failed 4");
                rval = 4;
            }
            Undo3();
        } else {
            printf("Failed 3");
            rval = 3;
        }
        Undo2();
    } else {
        printf("Failed 2");
        rval = 2;
    }
    Undo1();
} else {
    printf("Failed 1");
    rval = 1;
}
return rval;

Usually, for me, the DoX() are some kind of resource acquisition, such as malloc(), and the UndoX() are corresponding resource releases that should be performed only in the event of failure. The nesting clearly shows the association between corresponding acquisitions and releases, and avoids the need for repetition of the code for undo operations. It's also very easy to write -- you don't need to create or maintain labels, and it's easy to put the resource release in the right place as soon as you write the acquisition.

This approach does sometimes produce deeply nested code. That doesn't bother me much, but you might consider it an issue.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • 4
    'deeply nested' - *plus* error and error handling get separated by quite some distance, the more for the less deeply nested ones... – Aconcagua Nov 24 '18 at 08:52
  • @Aconcagua, separation of error and error-handling is a pretty consistent characteristic of all of these approaches. It follows directly from avoiding repetition of the error-handling code. – John Bollinger Nov 24 '18 at 15:19
  • This is what should ***not*** be done. Unreadable and error-prone because real code is not `DoX` and `UndoX`. – Acorn Jun 11 '21 at 10:41
  • This is definitely my single most controversial answer. – John Bollinger Jun 11 '21 at 13:00
0

This question is already overburdened with answers, but I would ike to point out that some codebases actually have wrapper code to deal with -what basically are- exceptions, in clean ways. For example, MuPdf implemented some trickery using longjmp's that emulate exception handling. In my opinion, if it comes to that, they should just be using C++ already, but that's just me.

You could try to do such wrappers yourself. As an exercise, let's think about your requirements and try to come up with a (very) crude design that tries to satisfy them:

  • We have a set of operations that need to be undone if subsequent operations fail;
  • Multiple operations must be undone in the reverse order that they were done;
  • The operation that failed must not be undone. It failed, after all;
  • Operations that were never reached must also not be undone, since they were never done in the first place.
  • Ideally, allow the programmer to be explicit: He knows what operations need to be undone, and when they do.

I've come up with some macros to solve this problem:

#include <stdio.h>

// Define some variables to keep track of when an error happened, and how many operations should be undone.
// Names are "mangled" by prefixing them with try_. You probably should come up with a better mangling scheme than this.
#define BEGIN_TRY int try_done = 0, try_error = 0, try_count = 0

// Here's how this works:
// - First, count the expression as an operation that may need to be undone;
// - If no error occured yet, do the operation;
// - If it succeeds, count it as a "done" operation;
// - If it fails, signal the error
#define TRY(expression) try_count++; if(!try_error && !(expression)) try_done++; else try_error = 1

// Here we take advantage of the fact that the operations behave like a queue.
// This means that, no matter what, operations need to be undone in the same 
// order everytime, and if an operation needs to be undone when there 
// are N operations, it also needs to be undone when there are N+1 operations.
// So, we don't really need to maintain the queue, if the programmer puts the operations in the correct order already. We just
// need to know how many operations to undo, and how much total operations are there (because we need to start at the end)
#define ON_ERROR(expression) do { if(try_error && try_done >= try_count--) {try_done--; (expression);} } while(0)

// To simplify the test, the "jobs" that we will try to do just pass or fail depending on the argument passed.
int job(int result) {return result;}
void undo(int i) {printf("Undone %d.\n", i);}

#define PASS 0
#define FAIL 1

// Let's test this
int main() {
    BEGIN_TRY;

    // try toying with the order (and quantity) of these.
    // just remember that for each "TRY" there must be one "ON_ERROR"!
    TRY(job(PASS));
    TRY(job(PASS));
    TRY(job(FAIL));
    TRY(job(PASS));

    // Because only the first two operations succeeded, we should only see the effects of undo(2) and undo(1).
    ON_ERROR(undo(4));
    ON_ERROR(undo(3));
    ON_ERROR(undo(2));
    ON_ERROR(undo(1));
}

See it live!

I'm no C expert, so there's probably some bugs in this (writing safe macros is hard), but my point is: If you think about your requirements in detail, all you will have to do is to come up with a solution which satisfies all of them. Another point that can be made is: Much like goto, many people see macros as evil. Don't be one of them: If a macro will make your code clearer, easier to read, then, by all means, use it.

Not a real meerkat
  • 5,604
  • 1
  • 24
  • 55
  • This is a defer approach. Exceptions are more complex than this. – Acorn Jun 11 '21 at 10:33
  • @Acorn the question is not about exceptions: The scope of the problem is smaller. And I do mention that this is a very crude design - all it does is serve as an example on how to satisfy the requirements that I listed based on the question - nothing else. – Not a real meerkat Jun 11 '21 at 13:58
  • Yes, but you start this answer talking about exceptions, which gives the impression this solution has something to do with them. This is RAII, not exceptions, done in a bad and error prone way. – Acorn Jun 11 '21 at 14:24
  • @Acorn in no part of the answer I say it implements exceptions, and I even mention that if that is what you truly want, you should be be using C++. Even the example from MuPDF I mentioned is much more complex than the quick example that I gave here - I have no intention of giving a full exceptions implementation in an answer, as that is way too big of a scope. If you feel my answer is bad go ahead and downvote it - I don't intend on continuing this discussion or changing this answer. – Not a real meerkat Jun 11 '21 at 15:20
  • I never said your example claims to implement exceptions. I said it starts talking about exceptions and then shows an example of something very different, which can confuse a beginner. I do not think the answer is very bad, but it is confusing and tries to argue macros are fine for this use case (they are not, because there are way better solutions without macros). – Acorn Jun 11 '21 at 19:25
-1

If the functions return some kind of state pointer or handle (like most allocation & initialization functions would), you can quite cleanly do this without goto by giving initial values to variables. Then you can have a single deallocation function that can handle the case where only part of the resources has been allocated.

For example:

void *mymemoryblock = NULL;
FILE *myfile = NULL;
int mysocket = -1;

bool allocate_everything()
{
    mymemoryblock = malloc(1000);
    if (!mymemoryblock)
    {
        return false;
    }

    myfile = fopen("/file", "r");   
    if (!myfile)
    {
        return false;
    }

    mysocket = socket(AF_INET, SOCK_STREAM, 0);
    if (mysocket < 0)
    {
        return false;
    }

    return true;
}

void deallocate_everything()
{
    if (mysocket >= 0)
    {
        close(mysocket);
        mysocket = -1;
    }

    if (myfile)
    {
        fclose(myfile);
        myfile = NULL;
    }

    if (mymemoryblock)
    {
        free(mymemoryblock);
        mymemoryblock = NULL;
    }
}

And then just do:

if (allocate_everything())
{
    do_the_deed();
}
deallocate_everything();
jpa
  • 10,351
  • 1
  • 28
  • 45
  • *"If the functions return some kind of state pointer or handle..."* Yes, but it is not the general case. Further, your solution requires 3 function for each function that allocates resources, plus global variables (or passing things around). – Acorn Nov 23 '18 at 16:29
-1

TL;DR:

I believe it should be written as:

int main (void)
{
  int result = do_func();
  printf("Failed %d\n", result);
}

Detailed explanation:

If nothing can be assumed what-so-ever about the function types, we can't easily use an array of function pointers, which would otherwise be the correct answer.

Assuming all function types are incompatible, then we would have to wrap in the original obscure design containing all those non-compatible functions, inside something else.

We should make something that is readable, maintainable, fast. We should avoid tight coupling, so that the undo behavior of "Do_x" doesn't depend on the undo behavior of "Do_y".

int main (void)
{
  int result = do_func();
  printf("Failed %d\n", result);
}

Where do_func is the function doing all the calls required by the algorithm, and the printf is the UI output, separated from the algorithm logic.

do_func would be implemented like a wrapper function around the actual function calls, handling the outcome depending on the result:

(With gcc -O3, do_func is inlined in the caller, so there is no overhead for having 2 separate functions)

int do_it (void)
{
  if(Do1()) { return 1; };
  if(Do2()) { return 2; };
  if(Do3()) { return 3; };
  if(Do4()) { return 4; };
  if(Do5()) { return 5; };
  return 0;
}

int do_func (void)
{
  int result = do_it();
  if(result != 0)
  {
    undo[result-1]();
  }
  return result;
}

Here the specific behavior is controlled by the array undo, which is a wrapper around the various non-compatible functions. Which functions to to call, in which order, is all part of the specific behavior tied to each result code.

We need to tidy it all up, so that we can couple a certain behavior to a certain result code. Then when needed, we only change the code in one single place if the behavior should be changed during maintenance:

void Undo_stuff1 (void) { }
void Undo_stuff2 (void) { Undo1(); }
void Undo_stuff3 (void) { Undo2(); Undo1(); }
void Undo_stuff4 (void) { Undo3(); Undo2(); Undo1(); }
void Undo_stuff5 (void) { Undo4(); Undo3(); Undo2(); Undo1(); }

typedef void Undo_stuff_t (void);
static Undo_stuff_t* undo[5] = 
{ 
  Undo_stuff1, 
  Undo_stuff2, 
  Undo_stuff3, 
  Undo_stuff4, 
  Undo_stuff5, 
};

MCVE:

#include <stdbool.h>
#include <stdio.h>

// some nonsense functions:
bool Do1 (void) { puts(__func__); return false; }
bool Do2 (void) { puts(__func__); return false; }
bool Do3 (void) { puts(__func__); return false; }
bool Do4 (void) { puts(__func__); return false; }
bool Do5 (void) { puts(__func__); return true; }
void Undo1 (void) { puts(__func__); }
void Undo2 (void) { puts(__func__); }
void Undo3 (void) { puts(__func__); }
void Undo4 (void) { puts(__func__); }
void Undo5 (void) { puts(__func__); }

// wrappers specifying undo behavior:
void Undo_stuff1 (void) { }
void Undo_stuff2 (void) { Undo1(); }
void Undo_stuff3 (void) { Undo2(); Undo1(); }
void Undo_stuff4 (void) { Undo3(); Undo2(); Undo1(); }
void Undo_stuff5 (void) { Undo4(); Undo3(); Undo2(); Undo1(); }

typedef void Undo_stuff_t (void);
static Undo_stuff_t* undo[5] = 
{ 
  Undo_stuff1, 
  Undo_stuff2, 
  Undo_stuff3, 
  Undo_stuff4, 
  Undo_stuff5, 
};

int do_it (void)
{
  if(Do1()) { return 1; };
  if(Do2()) { return 2; };
  if(Do3()) { return 3; };
  if(Do4()) { return 4; };
  if(Do5()) { return 5; };
  return 0;
}

int do_func (void)
{
  int result = do_it();
  if(result != 0)
  {
    undo[result-1]();
  }
  return result;
}

int main (void)
{
  int result = do_func();
  printf("Failed %d\n", result);
}

Output:

Do1
Do2
Do3
Do4
Do5
Undo4
Undo3
Undo2
Undo1
Failed 5
Lundin
  • 195,001
  • 40
  • 254
  • 396
  • 13
    So, for each single function in your code that allocates resources, you are going to write 4 functions instead (which, some of them duplicate `UndoN()` calls in turn). Plus a wrapper. Plus a type. Plus a global array of that type. No further comments. – Acorn Nov 23 '18 at 16:25
  • @Acorn No, for every ADT in my code, I'm going to write a user-friend API, a clarity of who's responsible for allocation/deallocation, private encapsulation, modular, maintainable code, without tight coupling, where everything is autonomous. In this particular case, it looks like we are patching up some old, broken design, in which case you need wrappers - which is the fault of the original (lack of) design. Of course you don't need that level of abstraction if it's just some quick & dirty hack, but in that case all that is best practices is thrown out the window anyway. – Lundin Nov 25 '18 at 15:11
-1

Here is an answer that I have found resilient to bugs.

Yes. It uses goto. I firmly believe you should use what gives you most clarity, rather than just blindly following the advice of those before you (goto as a construct can make spaghetti code, but in this instance every other error handling method ususally ends up more spaghetti-like than using this method of goto, so IMO it's superior).

Some people may not like the form of this code, but I contest that when used to the style it is cleaner, easier to read (when everything's lined up, of course), and much more resilient to errors. If you have the properly linter/static analysis setup, and you're working with POSIX, it pretty much requires you to code in this fashion to allow for good error handling.

static char *readbuf(char *path)
{
    struct stat st;
    char *s = NULL;
    size_t size = 0;
    int fd = -1;

    if (!path) { return NULL; }

    if ((stat(path, &st)) < 0) { perror(path); goto _throw; }

    size = st.st_size;
    if (size == 0) { printf("%s is empty!\n", path); goto _throw; }

    if (!(s = calloc(size, 1))) { perror("calloc"); goto _throw; }

    fd = open(path, O_RDONLY);
    if (fd < -1) { perror(path); goto _throw; }
    if ((read(fd, s, size)) < 0) { perror("read"); goto _throw; }
    close(fd); /* There's really no point checking close for errors */

    return s;

_throw:
    if (fd > 0) close(fd);
    if (s) free(s);
    return NULL;
}
Aster
  • 227
  • 1
  • 13
  • This introduces branches and can only be used if there is a way to check if something needs to be undone. – Acorn Jun 11 '21 at 10:44
  • "*There's really no point checking `close` for errors*" Closing can fail. – Acorn Jun 11 '21 at 10:44
  • Yes but what exactly are you going to do about it? Do most users know how to cope with that? Who these days is going to bother with manually closing said file? – Aster Jun 16 '21 at 09:29
  • If the file failed closing and you ignore it, it's likely you will ***silently*** lose data. Your users will be very annoyed to say the least. – Acorn Jun 16 '21 at 11:47
  • It is the worst kind of bug. Software written like that isn't reliable and cannot be trusted. – Acorn Jun 16 '21 at 11:49
  • If a write() call has finished, and an sync() call has finished -- both with no errors, then you won't lose any data aside from hard-disk problems (It's actually impossible to ask that any kernel write data to the disk, and it's impossible for the kernel to know if a disk has written data full stop). Now, on the matter of it not being reliable, it's just a file descriptor. – Aster Jun 22 '21 at 16:35
  • If you go ahead and read the close() spec -- there are only three conditions under which it can fail. First, if the descriptor is bad. If that's the case then you should *not* write a successive close, and there really isn't anything anyone can do about it. Second, if the close() was interrupted. Linux will itself invalidate the fd in this case, and when the program ends the descriptor will be cleaned up, so we do not have to worry. Third, if I/O fails. But if a prior write() succeeded then that's not important either -- either the earlier write() failed, or it did not. No data is lost. – Aster Jun 22 '21 at 16:42
  • The best you can do in any of these circumstances is either retry (Which is advised against because according to the spec the fd is unspecified in all of these circumstances, which gives you a double-close bug), or print a mostly-meaningless message to the user. If the drive is bad, most users will have found that out anyway. None of this is useful or interesting to report. The associated SO post reinforces this viewpoint, and advises against retrying on close() - https://stackoverflow.com/a/33114363/1235740 And this post covers other UNIXen: https://news.ycombinator.com/item?id=3364066 – Aster Jun 22 '21 at 16:46
  • "*If a write() call has finished, and an sync() call has finished -- both with no errors, then you won't lose any data aside from hard-disk problems*" This is false. – Acorn Jun 22 '21 at 18:12
  • "*Third, if I/O fails. But if a prior write() succeeded then that's not important either -- either the earlier write() failed, or it did not. No data is lost.*" Wrong too, even if you consider Linux only. – Acorn Jun 22 '21 at 18:15
  • "*print a mostly-meaningless message to the user. (...) None of this is useful or interesting to report. *" Meaningless? Losing data is meaningless for you? Not reporting critical error conditions is not useful? Wow! – Acorn Jun 22 '21 at 18:15
  • "*If the drive is bad, most users will have found that out anyway.*" Or not. However it doesn't matter for this discussion. Your app would be lying regardless. – Acorn Jun 22 '21 at 18:18
  • "*The associated SO post reinforces this viewpoint, and advises against retrying on close()*" There are many wrong viewpoints in SO, including yours. Nobody has talked about retrying. – Acorn Jun 22 '21 at 18:21
  • You should provide citations for your claims, as I have. Otherwise, anything you have said thusfar should be disregarded, since you have no evidence to back it up. Where do you get the idea from that write() can fail and yet not signal an error? Where do you get the idea from that a close() that failed inherently means that data has been lost? What do you advise the user do? What do you advise the programmer do? You have not answered any of my questions, simply stated I was 'wrong'. You had enough time to waste 10 minutes of it stating I was wrong, so you surely have enough to state *why*. – Aster Jun 30 '21 at 14:06
  • "*Where do you get the idea from that write() can fail and yet not signal an error?*" I didn't say that. *Writing the data* can still fail. – Acorn Jul 01 '21 at 10:55
  • "*Where do you get the idea from that a close() that failed inherently means that data has been lost?*" I didn't say that. Data *may* have been lost. – Acorn Jul 01 '21 at 10:55
  • "*What do you advise the user do?*" That depends on the program... – Acorn Jul 01 '21 at 10:56
  • "*What do you advise the programmer do?*" It isnt clear??? Dont ignore errors. – Acorn Jul 01 '21 at 10:58
  • "*You should provide citations for your claims, as I have.*" You havent, you linked SO and HN. My citations: the POSIX standard and the manual pages of your operating system. – Acorn Jul 01 '21 at 11:00
  • Per the Linux manual pages: "A successful close does not guarantee that the data has been successfully saved to disk, as the kernel uses the buffer cache to defer writes. [...] If you need to be sure that the data is physically stored on the underlying disk, use fsync(2)." - So exactly what I said earlier. It's your fault if you do not use fsync() before close(). Retrying close is bad practice, since the file descriptor is undefined. Retrying write is bad practice, since the file descriptor is undefined. All that can be done is telling the user, and most users do not understand what to do. – Aster Jul 06 '21 at 08:42
  • Also, I linked SO and HN, which had links to the LKML, POSIX, and other authorities that all back up exactly what I say -- on most POSIX conforming operating systems, there is nothing you, as a programmer, can do if close() signals an error on return, aside from warn the user. I contest that there is no use even doing that since the majority of users do not know what to do, and signalling non-useful errors (i.e. Errors with unclear remediations) just teaches and conditions a user to ignore errors, which is a net worse outcome. – Aster Jul 06 '21 at 08:44
  • Of course in any *properly structured program*, you would still write that as part of either debug, or when the user sets an 'extremely verbose' log setting. But this is a snippet on Stack Overflow, and it's very literally an example of a practical style of writing code, it is not meant to be about the semantics of close() or write() or anything *specific*. You haven't presented any actual reasoning thusfar in the specifics of what you say, and the evidence you've presented are literally the same documents I have presented, so it's really not worth listening to you any longer, good day! :) – Aster Jul 06 '21 at 08:47
  • "*It's your fault if you do not use fsync() before close()*" Handful messages ago you were arguing silently losing data is fine cause nothing can be done about it... now you`re learning about fsync(), error conditions on fsync() and close(), POSIX, etc. Well done, you are making progress! I am glad you reconsidered your position :) – Acorn Jul 06 '21 at 22:41
-2
typedef void(*undoer)();
int undo( undoer*const* list ) {
  while(*list) {
    (*list)();
    ++list;
  }
}
void undo_push( undoer** list, undoer* undo ) {
  if (!undo) return;
  // swap
  undoer* tmp = *list;
  *list = undo;
  undo = tmp;
  undo_push( list+1, undo );
}
int func() {
  undoer undoers[6]={0};

  if (Do1()) { printf("Failed 1"); return 1; }
  undo_push( undoers, Undo1 );
  if (Do2()) { undo(undoers); printf("Failed 2"); return 2; }
  undo_push( undoers, Undo2 );
  if (Do3()) { undo(undoers); printf("Failed 3"); return 3; }
  undo_push( undoers, Undo3 );
  if (Do4()) { undo(undoers); printf("Failed 4"); return 4; }
  undo_push( undoers, Undo4 );
  if (Do5()) { undo(undoers); printf("Failed 5"); return 5; }
  return 6;
}

I made undo_push do the O(n) work. This is less efficient than having undo do the O(n) work, as we expect more push's than undos. But this version was a touch simpler.

A more industrial strength version would have head and tail pointers and even capacity.

The basic idea is to keep a queue of undo actions in a stack, then execute them if you need to clean up.

Everything is local here, so we don't pollute global state.


struct undoer {
  void(*action)(void*);
  void(*cleanup)(void*);
  void* state;
};

struct undoers {
  undoer* top;
  undoer buff[5];
};
void undo( undoers u ) {
  while (u.top != buff) 
  {
    (u.top->action)(u.top->state);
    if (u.top->cleanup)
      (u.top->cleanup)(u.top->state);
    --u.top;
  }
}
void pundo(void* pu) {
  undo( *(undoers*)pu );
  free(pu);
}
void cleanup_undoers(undoers u) {
  while (u.top != buff) 
  {
    if (u.top->cleanup)
      (u.top->cleanup)(u.top->state);
    --u.top;
  }
}
void pcleanup_undoers(void* pu) {
  cleanup_undoers(*(undoers*)pu);
  free(pu);
}
void push_undoer( undoers* to_here, undoer u ) {
  if (to_here->top != (to_here->buff+5))
  {
    to_here->top = u;
    ++(to_here->top)
    return;
  }
  undoers* chain = (undoers*)malloc( sizeof(undoers) );
  memcpy(chain, to_here, sizeof(undoers));
  memset(to_here, 0, sizeof(undoers));
  undoer chainer;
  chainer.action = pundo;
  chainer.cleanup = pcleanup_undoers;
  chainer.data = chain;
  push_undoer( to_here, chainer );
  push_undoer( to_here, u );
}
void paction( void* p ) {
  (void)(*a)() = ((void)(*)());
  a();
}
void push_undo( undoers* to_here, void(*action)() ) {
  undor u;
  u.action = paction;
  u.cleanup = 0;
  u.data = (void*)action;
  push_undoer(to_here, u);
}

then you get:

int func() {
  undoers u={0};

  if (Do1()) { printf("Failed 1"); return 1; }
  push_undo( &u, Undo1 );
  if (Do2()) { undo(u); printf("Failed 2"); return 2; }
  push_undo( &u, Undo2 );
  if (Do3()) { undo(u); printf("Failed 3"); return 3; }
  push_undo( &u, Undo3 );
  if (Do4()) { undo(u); printf("Failed 4"); return 4; }
  push_undo( &u, Undo4 );
  if (Do5()) { undo(u); printf("Failed 5"); return 5; }
  cleanup_undoers(u);
  return 6;
}

but that is getting ridiculous.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    More complex, requires that `DoN`/`UndoN` are actual functions (and the same signature), requires stack space, slower. – Acorn Nov 23 '18 at 17:09
-4

Let's try for something with zero curly braces:

int result;
result =                   Do1() ? 1 : 0;
result = result ? result : Do2() ? 2 : 0;
result = result ? result : Do3() ? 3 : 0;
result = result ? result : Do4() ? 4 : 0;
result = result ? result : Do5() ? 5 : 0;

result > 4 ? (Undo5(),0) : 0;
result > 3 ? Undo4() : 0;
result > 2 ? Undo3() : 0;
result > 1 ? Undo2() : 0;
result > 0 ? Undo1() : 0;

result ? printf("Failed %d\r\n", result) : 0;

Yes. 0; is a valid statement in C (and C++). In the case that some of the functions return something that is incompatible with this syntax (e.g. void perhaps) then the Undo5() style can be used.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Chris Becke
  • 34,244
  • 12
  • 79
  • 148
  • This assumes the `UndoN` functions return values, when in fact they may be (and most probably are) declared `void` (or aren't even functions at all). – Not a real meerkat Nov 23 '18 at 19:44
  • msvc is never a particularly standards compliant compiler, but without thinking about it I did actually develop this with void Undo functions. No idea if its actually valid. If it isn't one could just go with: `result > 4 ? (Undo5(), 0) : 0; Doesn't help of course. if 'UndoX' isn't actually a function. – Chris Becke Nov 23 '18 at 19:52
  • yeah, MSVC is a [bad, bad boy](https://godbolt.org/z/AhQXi8), for C++ at least. In C, this seems to be valid. My bad. – Not a real meerkat Nov 23 '18 at 19:56
  • 2
    I would argue that `if (result > 4) Undo5();` is easier to understand than a ternary conditional with no false action and a discarded result. (`if` statements don't *need* curly braces) – pizzapants184 Nov 24 '18 at 01:56
  • True. I really was avoiding any kind of explicit flow control. ternary conditions are cheating I know. – Chris Becke Nov 24 '18 at 13:54
-7

A sane (no gotos, no nested or chained ifs) approach would be

int bar(void)
{
  int rc = 0;

  do
  { 
    if (do1())
    {
      rc = 1;
      break;        
    }

    if (do2())
    {
      rc = 2;
      break;        
    }

    ...

    if (do5())
    {
      rc = 5;
      break;        
    }
  } while (0);

  if (rc)
  {
    /* More or less stolen from Chris' answer: 
       https://stackoverflow.com/a/53444967/694576) */
    switch(rc - 1)
    {
      case 5: /* Not needed for this example, but left in in case we'd add do6() ... */
        undo5();

      case 4:
        undo4();

      case 3:
        undo3();

      case 2:
        undo2();

      case 1:
        undo1();

      default:
        break;
    }
  }

  return rc;
}
alk
  • 69,737
  • 10
  • 105
  • 255
  • 10
    If your definition of "sane" is "no goto" then you already failed because sanest way to handle this is indeed to use goto. – Purple Ice Nov 23 '18 at 11:51
  • @PurpleIce: You are probably right for simple cases like the OP's. But the moment we have several such things woven into each other using goto is far to error prone. And yes, this latter case could be considered a design issue. – alk Nov 23 '18 at 13:53
  • 3
    @alk The `goto` solution scales linearly to any complexity, as shown in other answers, just like this one, but with less clutter and extraneous loops and branches. – Acorn Nov 23 '18 at 13:58
  • 4
    The `do { ... } while (0)` used here is just an obfuscated way of writing a `goto`. There’s no advantage at all compared to using `goto`, and it’s quite a bit harder to read. – NobodyNada Nov 23 '18 at 22:43