Чистые способы сделать несколько отмен в C - программирование
Подтвердить что ты не робот

Чистые способы сделать несколько отмен в C

Кто-то, вероятно, скажет что-то об исключениях... но в C, каковы другие способы сделать следующее чисто/ясно и без повторения такого большого количества кода?

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...

Это может быть одним из случаев использования gotos. Или, может быть, несколько внутренних функций...

4b9b3361

Ответ 1

Да, в таких случаях довольно часто использовать goto, чтобы избежать повторения.

Пример:

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;
}

Как всегда, вы, вероятно, также хотите, чтобы ваши функции были небольшими и содержательно объединяли операции, чтобы избежать большого "кода отмены".

Ответ 2

Это может быть одним из случаев использования gotos.

Конечно, давайте попробуем это. Вот возможная реализация:

#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;
}

Ответ 3

Если у вас одинаковая подпись для вашей функции, вы можете сделать что-то вроде этого:

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);
}

Вы можете изменить результат возврата одной из функций Do, чтобы увидеть, как она реагирует :)

Ответ 4

Для полноты немного запутывания:

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;
}

Ответ 5

Используйте goto для управления очисткой в C.

Например, проверьте стиль кодирования ядра Linux:

Обоснование использования goto:

  • безусловные утверждения легче понять, а последующее вложение уменьшается
  • ошибки, не обновляя отдельные точки выхода при внесении изменений, предотвращаются
  • сохраняет работу компилятора для оптимизации избыточного кода;)

Пример:

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;
}

В вашем конкретном случае это может выглядеть так:

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;
}

Ответ 6

Вероятно, есть много способов сделать это, но одна идея состоит в том, что если вы не вызовете одну функцию, если предыдущая не будет выполнена успешно, вы можете связать вызовы функций, используя else if это так. И используя переменную, чтобы отследить, где она терпит неудачу, вы можете также использовать оператор switch для отката.

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;

Ответ 7

Да, как объясняется другими ответами, использование goto для обработки ошибок часто уместно в C.

Тем не менее, если возможно, вы, вероятно, должны сделать свой код очистки безопасным для вызова, даже если соответствующее действие никогда не выполнялось. Например, вместо:

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 we don't need p3 anymore.
    free(p3);    

    return 0;

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

Я бы защищал:

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(p1);
    }
    return result;
}

Он немного менее эффективен, поскольку требует инициализации переменных NULL, но он более удобен в обслуживании, поскольку вам не нужны дополнительные метки. Там меньше вещей, чтобы ошибиться при внесении изменений в код. Кроме того, если есть код очистки, который вам нужен как на пути успеха, так и на пути к неудаче, вы можете избежать дублирования кода.

Ответ 8

Обычно я подхожу к такой проблеме, вкладывая условные выражения:

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;

Обычно для меня DoX() - это какое-то приобретение ресурсов, например malloc(), а UndoX() - соответствующие выпуски ресурсов, которые должны выполняться только в случае сбоя. Вложенность четко показывает связь между соответствующими приобретениями и выпусками и устраняет необходимость повторения кода для операций отмены. Его также очень легко написать - вам не нужно создавать или поддерживать ярлыки, и легко разместить ресурс в нужном месте, как только вы напишете приобретение.

Этот подход иногда создает глубоко вложенный код. Это не сильно беспокоит меня, но вы можете считать это проблемой.

Ответ 9

Вот ответ, который я нашел устойчивым к ошибкам.

Да. Это использует goto. Я твердо верю, что вы должны использовать то, что дает вам наибольшую ясность, а не просто слепо следовать советам тех, кто был до вас (goto как конструкция может создать код для спагетти, но в этом случае любой другой метод обработки ошибок обычно заканчивается более похожим на спагетти, чем используя этот метод goto, так что IMO его превосходит).

Некоторым людям может не понравиться форма этого кода, но я оспариваю, что когда он привык к стилю, он чище, его легче читать (когда все выстраивается в очередь, конечно) и гораздо более устойчивы к ошибкам. Если у вас правильно настроен статический/статический анализ, и вы работаете с POSIX, вам в значительной степени требуется, чтобы вы кодировали таким образом, чтобы обеспечить хорошую обработку ошибок.

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 really no point checking close for errors */

    return s;

_throw:
    if (fd > 0) close(fd);
    if (s) free(s);
    return NULL;
}

Ответ 10

Этот вопрос уже перегружен ответами, но я хотел бы отметить, что некоторые кодовые базы на самом деле имеют код-обертку, чтобы иметь дело с -what в основном are- исключениями, в чистом виде. Например, MuPdf реализовал некоторые хитрости, используя longjmp, который эмулирует обработку исключений. На мой взгляд, если до этого дойдет, они уже должны просто использовать C++, но это только я.

Вы можете попробовать сделать такие обертки самостоятельно. В качестве упражнения давайте подумаем о ваших требованиях и постараемся придумать (очень) грубый дизайн, который пытается их удовлетворить:

  • У нас есть набор операций, которые необходимо отменить, если последующие операции завершатся неудачно;
  • Несколько операций должны быть отменены в обратном порядке, в котором они были выполнены;
  • Операция, которую не удалось не должно быть отменено. В конце концов, это не удалось;
  • Операции, которые никогда не были достигнуты, также не должны быть отменены, поскольку они никогда не выполнялись.
  • В идеале позвольте программисту быть явным: он знает, какие операции нужно отменить, и когда они это делают.

Я придумал несколько макросов для решения этой проблемы:

#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 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 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));
}

Смотрите это в прямом эфире!

Я не эксперт по Си, поэтому, возможно, в этом есть некоторые ошибки (писать безопасные макросы сложно), но моя точка зрения такова: если вы подумаете о своих требованиях подробно, все, что вам нужно сделать, - это найти решение, которое удовлетворяет их всех. Можно сделать еще одно замечание: очень похоже на goto, многие люди видят макросы как зло. Не будьте одним из них: если макрос сделает ваш код более понятным, более легким для чтения, тогда, безусловно, используйте его.

Ответ 11

Если функции возвращают какой-либо указатель или дескриптор состояния (как это делает большинство функций выделения и инициализации), вы можете совершенно просто сделать это без goto, передав начальные значения переменным. Тогда у вас может быть одна функция освобождения, которая может обрабатывать случай, когда была выделена только часть ресурсов.

Например:

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;
    }
}

А потом просто сделайте:

if (allocate_everything())
{
    do_the_deed();
}
deallocate_everything();

Ответ 12

TL; DR:

Я считаю, что это должно быть написано как:

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

Детальное объяснение:

Если ничего нельзя предположить о типах функций, мы не можем легко использовать массив указателей на функции, которые в противном случае были бы правильным ответом.

Предполагая, что все типы функций несовместимы, мы должны были бы обернуть оригинальный неясный дизайн, содержащий все эти несовместимые функции, во что-то еще.

Мы должны сделать что-то читаемое, удобное в обслуживании, быстрое. Мы должны избегать жесткой связи, чтобы поведение отмены "Do_x" не зависело от поведения отмены "Do_y".

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

Где do_func - это функция, выполняющая все вызовы, требуемые алгоритмом, а printf - это вывод пользовательского интерфейса, отделенный от логики алгоритма.

do_func будет реализован как функция-обертка вокруг реальных вызовов функций, обрабатывая результат в зависимости от результата:

(С помощью gcc -O3 do_func встроен в вызывающую программу, поэтому нет необходимости использовать две отдельные функции)

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;
}

Здесь конкретное поведение контролируется массивом undo, который является оболочкой для различных несовместимых функций. Какие функции для вызова, в каком порядке, являются частью конкретного поведения, связанного с каждым кодом результата.

Нам нужно все привести в порядок, чтобы мы могли связать определенное поведение с определенным кодом результата. Затем, когда это необходимо, мы изменяем код только в одном месте, если поведение должно быть изменено во время обслуживания:

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);
}

Выход:

Do1
Do2
Do3
Do4
Do5
Undo4
Undo3
Undo2
Undo1
Failed 5

Ответ 13

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;
}

Я заставил undo_push выполнять работу O (n). Это менее эффективно, чем undo выполнения O (n), так как мы ожидаем большего нажатия, чем отмены. Но эта версия была немного проще.

У более промышленной версии силы были бы указатели головы и хвоста и даже способность.

Основная идея состоит в том, чтобы сохранить очередь отмененных действий в стеке, а затем выполнить их, если вам нужно очистить.

Здесь все локально, поэтому мы не загрязняем глобальное состояние.


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);
}

тогда вы получите:

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;
}

но это становится смешным.

Ответ 14

Давайте попробуем что-нибудь с нулевыми фигурными скобками:

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;

Да. 0; является допустимым утверждением в C (и C++). В случае, если некоторые функции возвращают что-то несовместимое с этим синтаксисом (например, возможно, void), можно использовать стиль Undo5().

Ответ 15

Разумный (без gotos, без вложенных или цепочечных ifs) подход будет

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;
}