干净的方法来在C中执行多个撤消

时间:2018-11-23 10:17:18

标签: 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的一种情况。也许有多个内部功能...

15 个答案:

答案 0 :(得分:49)

是的,在这种情况下使用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;
}

与往常一样,您可能还希望保持函数较小,并以有意义的方式将操作分批处理,以避免产生大量的“撤消代码”。

答案 1 :(得分:18)

  

这可能是使用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;
}

答案 2 :(得分:14)

如果您的函数具有相同的签名,则可以执行以下操作:

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函数之一的返回值,以查看其反应:)

答案 3 :(得分:8)

为了完整起见,有些混淆:

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

答案 4 :(得分:4)

使用goto来管理C语言中的清理。

例如,检查Linux kernel coding style

  

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

答案 5 :(得分:3)

执行此操作的方法可能很多,但是一个想法是,除非先执行一个函数,否则您将不会调用一个函数,因此可以使用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;

答案 6 :(得分:2)

是的,如其他答案所解释,在C语言中通常使用goto进行错误处理。

这就是说,如果可能的话,即使从未执行过相应的操作,您也应该使清理代码安全地调用。例如,代替:

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,因此效率稍低,但由于不需要额外的标签,因此更易于维护。更改代码时,出错的地方更少了。另外,如果在成功的失败路径上都需要清理代码,则可以避免代码重复。

答案 7 :(得分:1)

这个问题已经不堪重负了,但是我要指出的是,一些代码库实际上具有包装器代码,以干净的方式处理-基本上是-异常。例如,MuPdf implemented some trickery using longjmp's that emulate exception handling。我认为,如果涉及到这一点,他们应该已经在使用C ++,但这就是我。

您可以尝试自己做此类包装。作为练习,让我们考虑一下您的需求,并尝试提出一个(非常)粗略的设计来满足他们的需求:

  • 我们有一系列操作,如果后续操作失败,则需要撤消;
  • 必须按照相反的顺序撤消多个操作;
  • 失败的操作必须被撤消。毕竟失败了;
  • 从未完成的操作也必须撤消,因为它们从来没有做过。
  • 理想情况下,允许程序员明确:他知道需要撤消哪些操作以及何时撤消操作。

我想出了一些宏来解决这个问题:

    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <version>4.2.0</version>
    </dependency>

    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-core</artifactId>
        <version>4.2.0</version>
    </dependency>

    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit</artifactId>
        <version>4.2.0</version>
    </dependency>

See it live!

我不是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'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)); } 一样,许多人将宏视为邪恶。不要成为其中之一:如果宏可以使您的代码更清晰,更易于阅读,则请务必使用它。

答案 8 :(得分:0)

我通常通过嵌套条件来解决此类问题:

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 :(得分:0)

这是我发现可以抵抗错误的答案。

是的。它使用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's really no point checking close for errors */

    return s;

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

答案 10 :(得分:-1)

如果函数返回某种状态指针或句柄(就像大多数分配和初始化函数一样),则可以通过给变量赋初始值而无需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();

答案 11 :(得分:-1)

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是UI输出,与算法逻辑分开。

do_func就像实际函数调用周围的包装函数一样实现,根据结果处理结果:

(对于gcc -O3,do_func被内联在调用方中,因此拥有2个单独的函数没有开销)

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

答案 12 :(得分:-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;
}

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

但这太荒谬了。

答案 13 :(得分:-4)

让我们尝试大括号为零的东西:

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()样式。

答案 14 :(得分:-7)

一个理智的方法(没有getos,没有嵌套或链接的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;
}