在C中有效使用goto进行错误pipe理?
这个问题其实是前段时间在programming.reddit.com上有趣的讨论的结果。 它基本上归结为以下代码:
int foo(int bar) { int return_value = 0; if (!do_something( bar )) { goto error_1; } if (!init_stuff( bar )) { goto error_2; } if (!prepare_stuff( bar )) { goto error_3; } return_value = do_the_thing( bar ); error_3: cleanup_3(); error_2: cleanup_2(); error_1: cleanup_1(); return return_value; }
goto
这里的使用似乎是最好的方式,导致所有可能性的最干净和最有效的代码,或者至less在我看来。 引用史蒂夫McConnell在代码完成 :
goto在分配资源,对这些资源执行操作,然后释放资源的例程中非常有用。 随着转到,你可以清理代码的一部分。 goto减less了您忘记在每个检测到错误的地方释放资源的可能性。
此方法的另一个支持来自本部分的“ Linux设备驱动程序”一书。
你怎么看? 这种情况下,有效的使用C goto
? 你会更喜欢其他的方法,这会产生更复杂和/或效率更低的代码,但是避免goto
?
FWIF,我发现在这个问题的例子中,你给出的error handling习惯用法比迄今为止答案中给出的任何可选方法都更易读易懂。 虽然goto
在一般情况下是个不错的主意,但是当以简单而统一的方式完成error handling时,它会非常有用。 在这种情况下,即使它是一个转换,它被用于定义明确,或多或less结构化的方式。
作为一般规则,避免转向是一个好主意,但是当Dijkstra第一次写下“被认为有害的GOTO”的时候,stream行的弊端甚至不能作为大多数人的想法的select。
你所概述的是error handling问题的一个通用的解决scheme – 只要仔细使用它,这对我来说是很好的。
您的具体示例可以简化如下(步骤1):
int foo(int bar) { int return_value = 0; if (!do_something(bar)) { goto error_1; } if (!init_stuff(bar)) { goto error_2; } if (prepare_stuff(bar)) { return_value = do_the_thing(bar); cleanup_3(); } error_2: cleanup_2(); error_1: cleanup_1(); return return_value; }
继续这个过程:
int foo(int bar) { int return_value = 0; if (do_something(bar)) { if (init_stuff(bar)) { if (prepare_stuff(bar)) { return_value = do_the_thing(bar); cleanup_3(); } cleanup_2(); } cleanup_1(); } return return_value; }
我相信这相当于原来的代码。 这看起来特别干净,因为原来的代码本身非常干净,组织良好。 通常情况下,代码片段并不像那样(尽pipe我接受一个他们应该是的论点)。 例如,经常有更多的状态传递给初始化(设置)程序比所示,因此更多的状态也传递给清理程序。
我很惊讶没有人提出这个select,所以即使这个问题已经存在了一段时间了,我会join它:解决这个问题的一个好方法是使用variables来跟踪当前状态。 这是一种可以使用的技术,无论是否使用goto
来达到清理代码。 就像任何编码技术一样,它有利有弊,并不适用于任何情况,但是如果你select了一种风格,那么值得考虑 – 特别是如果你想避免转向,而不是结束深嵌套的if
。
其基本思想是,对于每一个可能需要采取的清理行动,都有一个variables,从中可以看出清理是否需要执行。
我将首先显示goto
版本,因为它更接近于原始问题中的代码。
int foo(int bar) { int return_value = 0; int something_done = 0; int stuff_inited = 0; int stuff_prepared = 0; /* * Prepare */ if (do_something(bar)) { something_done = 1; } else { goto cleanup; } if (init_stuff(bar)) { stuff_inited = 1; } else { goto cleanup; } if (prepare_stuff(bar)) { stufF_prepared = 1; } else { goto cleanup; } /* * Do the thing */ return_value = do_the_thing(bar); /* * Clean up */ cleanup: if (stuff_prepared) { unprepare_stuff(); } if (stuff_inited) { uninit_stuff(); } if (something_done) { undo_something(); } return return_value; }
与其他技术相比,其中一个优点是,如果初始化函数的顺序发生改变,正确的清理仍然会发生 – 例如,使用另一个答案中描述的switch
方法,如果初始化顺序改变,那么switch
必须非常仔细地编辑,以避免尝试清理某些东西实际上并没有初始化。
现在,有人可能会争辩说,这个方法增加了很多额外的variables – 实际上在这种情况下是这样 – 但实际上通常现有的variables已经跟踪或者可以跟踪所需的状态。 例如,如果prepare_stuff()
实际上是对malloc()
或open()
的调用,则可以使用保存返回的指针或文件描述符的variables,例如:
int fd = -1; .... fd = open(...); if (fd == -1) { goto cleanup; } ... cleanup: if (fd != -1) { close(fd); }
现在,如果我们另外跟踪一个variables的错误状态,我们可以完全避免goto
,并且仍然正确地清理,而不会有越来越深的缩进,我们需要更多的初始化:
int foo(int bar) { int return_value = 0; int something_done = 0; int stuff_inited = 0; int stuff_prepared = 0; int oksofar = 1; /* * Prepare */ if (oksofar) { /* NB This "if" statement is optional (it always executes) but included for consistency */ if (do_something(bar)) { something_done = 1; } else { oksofar = 0; } } if (oksofar) { if (init_stuff(bar)) { stuff_inited = 1; } else { oksofar = 0; } } if (oksofar) { if (prepare_stuff(bar)) { stuff_prepared = 1; } else { oksofar = 0; } } /* * Do the thing */ if (oksofar) { return_value = do_the_thing(bar); } /* * Clean up */ if (stuff_prepared) { unprepare_stuff(); } if (stuff_inited) { uninit_stuff(); } if (something_done) { undo_something(); } return return_value; }
再次,有这样的潜在批评:
- 难道不是所有这些“如果”伤害performance? 不 – 因为在成功的情况下,你必须做所有的检查(否则你不检查所有的错误情况); 而在失败的情况下,大多数编译器会优化顺序失败,
if (oksofar)
检查一次跳转到清理代码(GCC肯定if (oksofar)
) – 在任何情况下,错误情况通常对性能来说都不太重要。 -
这不是增加了另一个variables吗? 在这种情况下,是的,但通常
return_value
variables可以用来扮演oksofar
在这里玩的angular色。 如果你构build你的函数来以一致的方式返回错误,你甚至可以在每种情况下避免第二个错误:int return_value = 0; if (!return_value) { return_value = do_something(bar); } if (!return_value) { return_value = init_stuff(bar); } if (!return_value) { return_value = prepare_stuff(bar); }
这样编码的好处之一就是一致性意味着原程序员忘记检查返回值的任何地方都会像拇指一样伸出手来,这使得更容易find(即一类)错误。
所以 – 这是(还)一个可以用来解决这个问题的风格。 使用正确,它允许非常干净,一致的代码 – 就像任何技术,在错误的手中它可以最终产生冗长的和令人困惑的代码:-)
goto
关键字的问题大部分被误解了。 这不是邪恶的。 您只需要注意您在每次跳转时创build的额外控制path。 对你的代码进行推理变得困难,因此也是很有道理的。
FWIW,如果你查看developer.apple.com教程,他们采取goto方法来处理错误。
我们不使用gotos。 重视价值的重要性更高。 exception处理是通过setjmp/longjmp
完成的,只要你可以。
(void)*指针在道德上是错误的,goto语句没有任何道德上的错误。
这完全在你如何使用这个工具。 在你提出的(微不足道)情况下,case语句可以达到相同的逻辑,尽pipe会有更多的开销。 真正的问题是,“我的速度要求是什么?”
goto只是速度很快,特别是如果你小心翼翼地确保编译成一个短暂的跳跃。 完美的速度是一个溢价的应用程序。 对于其他应用程序来说,采用if / else + case维护性开销可能是有意义的。
记住:goto不会杀死应用程序,开发者会杀死应用程序。
更新:这是案例
int foo(int bar) { int return_value = 0 ; int failure_value = 0 ; if (!do_something(bar)) { failure_value = 1; } else if (!init_stuff(bar)) { failure_value = 2; } else if (prepare_stuff(bar)) { return_value = do_the_thing(bar); cleanup_3(); } switch (failure_value) { case 2: cleanup_2(); case 1: cleanup_1(); default: break ; } }
GOTO是有用的。 这是你的处理器可以做的事,这就是为什么你应该有权访问它。
有时你想添加一些东西到你的function和单个转到让你这么做。 它可以节省时间..
总的来说,我认为一个代码可以使用goto
作为一个症状 ,程序stream程可能比通常所希望的更为复杂,这个事实是最清晰的。 以奇怪的方式结合其他程序结构来避免使用goto
将试图治疗症状,而不是疾病。 如果没有goto
你的具体例子可能不会太困难:
做{ 设置只有在提前退出的情况下才需要清理的东西 如果(错误)中断; 做 { 设置提前退出时需要清理的东西2 如果(错误)中断; // *****看到这条线的文字 (0); 清理事物2; (0); 清理事物1;
但是如果只是在函数失败时才会发生清理,则可以通过在第一个目标标签之前放置一个return
来处理goto
情况。 上面的代码将需要在标有*****
的行添加return
。
在“正常情况下的清理”情景中,我会认为goto
的使用比do
/ while(0)
更清晰,除此之外,因为目标标签本身实际上喊出“看我”远远超过break
和do
/ while(0)
构造。 对于“只有在发生错误时才进行清理”的情况, return
语句从可读性的angular度来看,最终必须处于最差的可能位置(返回语句通常应该在函数的开头,否则就是“看起来像“ 结束); 在一个目标标签刚好在一个“循环”结束之前达到这个限定要比在一个目标标签之前有一个return
更容易。
顺便说一句,我有时使用goto
进行error handling的一种情况是在switch
语句中,当多个案例的代码共享相同的错误代码时。 即使我的编译器经常足够聪明,可以认识到多个情况以相同的代码结束,但我认为更清楚地说:
REPARSE_PACKET: 开关(分组[0]) { 情况PKT_THIS_OPERATION: 如果(问题条件) 转到PACKET_ERROR; ...处理THIS_OPERATION 打破; 案例PKT_THAT_OPERATION: 如果(问题条件) 转到PACKET_ERROR; 处理THAT_OPERATION 打破; ... 案件PKT_PROCESS_CONDITIONALLY 如果(packet_length <9) 转到PACKET_ERROR; 如果(packet_condition涉及包[4]) { packet_length - = 5; memmove(packet,packet + 5,packet_length); 转到REPARSE_PACKET; } 其他 { 数据包[0] = PKT_CONDITION_SKIPPED; packet [4] = packet_length; packet_length = 5; packet_status = READY_TO_SEND; } 打破; ... 默认: { PACKET_ERROR: packet_error_count ++; packet_length = 4; 数据包[0] = PKT_ERROR; packet_status = READY_TO_SEND; 打破; } }
尽pipe可以用{handle_error(); break;}
来代替goto
语句{handle_error(); break;}
{handle_error(); break;}
,尽pipe可以使用do
/ while(0)
循环以及continue
处理包装的条件执行数据包,但我并不认为这比使用goto
更清晰。 另外,尽pipe在使用goto PACKET_ERROR
PACKET_ERROR
任何地方都可以从PACKET_ERROR
复制代码,而编译器可能会将重复的代码写出一次,并用跳转到该共享副本的方式replace大部分事件,但是使用goto
更容易注意到将数据包设置稍微不同的地方(例如,如果“执行条件”指令决定不执行)。
我本人是“十大安全关键代码编写规范的十大力量”的追随者。
我将在文中join一小段内容,说明我认为是一个关于转到的好主意。
规则:将所有代码限制为非常简单的控制stream构造 – 不要使用goto语句,setjmp或longjmp构造,以及直接或间接recursion。
理由:更简单的控制stream程转化为更强大的validation能力,并且经常会提高代码的清晰度。 recursion的驱逐也许是这里最大的惊喜。 但是,如果没有recursion,我们保证有一个非循环函数调用图,它可以被代码分析器利用,并且可以直接帮助certificate所有应该被限制的执行实际上是有界的。 (请注意,这个规则并不要求所有的函数都有一个单一的返回点,虽然这也经常会简化控制stream程,但是有足够的例子,早期的错误返回是更简单的解决scheme。
放弃使用goto 似乎不好,但是:
如果这些规则起初看起来是非常严厉的 ,那么要记住,它们是为了能够检查代码,从字面上看你的生活可能取决于它的正确性:代码用于控制你飞行的飞机,核电站离你住的地方还有几英里,或者是宇航员带着宇航员进入轨道。 这些规则就像你的车上的安全带一样:最初他们可能有点不舒服,但一段时间后,他们的使用变成二手性,不使用它们变得不可思议。
我同意在问题中给出的相反顺序的清理是在大多数function中最清洁的方法。 但是我也想指出,有时候,你希望你的函数能够清理干净。 在这些情况下,如果使用(0){label:}成语进入清理过程的正确点,则使用以下变体:
int decode ( char * path_in , char * path_out ) { FILE * in , * out ; code c ; int len ; int res = 0 ; if ( path_in == NULL ) in = stdin ; else { if ( ( in = fopen ( path_in , "r" ) ) == NULL ) goto error_open_file_in ; } if ( path_out == NULL ) out = stdout ; else { if ( ( out = fopen ( path_out , "w" ) ) == NULL ) goto error_open_file_out ; } if( read_code ( in , & c , & longueur ) ) goto error_code_construction ; if ( decode_h ( in , c , out , longueur ) ) goto error_decode ; if ( 0 ) { error_decode: res = 1 ;} free_code ( c ) ; if ( 0 ) { error_code_construction: res = 1 ; } if ( out != stdout ) fclose ( stdout ) ; if ( 0 ) { error_open_file_out: res = 1 ; } if ( in != stdin ) fclose ( in ) ; if ( 0 ) { error_open_file_in: res = 1 ; } return res ; }
在我看来, cleanup_3
应该做清理,然后调用cleanup_2
。 同样, cleanup_2
应该做清理,然后调用cleanup_1。 看来,每当你做cleanup_[n]
时, cleanup_[n-1]
是必需的,因此它应该是方法的责任(所以,例如, cleanup_3
不能调用cleanup_2
而调用,并且可能导致泄漏。)
鉴于这种方法,而不是gotos,你可以简单地调用清理例程,然后返回。
goto
方法并不是错误或不好的 ,但是值得注意的是它不一定是“最干净”的方法(恕我直言)。
如果你正在寻找最佳性能,那么我认为goto
解决scheme是最好的。 然而,我只希望它是相关的,但是,在selectless数性能关键的应用程序(例如,设备驱动程序,embedded式设备等)中。 否则,这是一个比代码清晰度更低优先级的微优化。
我认为这里的问题对于给定的代码是错误的。
考虑:
- do_something(),init_stuff()和prepare_stuff()似乎知道它们是否失败,因为它们在这种情况下返回false或nil。
- 由于在foo()中没有直接build立状态,因此build立状态的责任似乎是这些function的责任。
因此:do_something(),init_stuff()和prepare_stuff()应该自己清理 。 有一个单独的cleanup_1()函数,在do_something()之后清除封装的哲学。 这是糟糕的devise。
如果他们做了自己的清理,那么foo()变得相当简单。
另一方面。 如果foo()实际上创build了自己需要拆卸的状态,那么转换就是合适的。
这是我所喜欢的:
bool do_something(void **ptr1, void **ptr2) { if (!ptr1 || !ptr2) { err("Missing arguments"); return false; } bool ret = false; //Pointers must be initialized as NULL void *some_pointer = NULL, *another_pointer = NULL; if (allocate_some_stuff(&some_pointer) != STUFF_OK) { err("allocate_some_stuff step1 failed, abort"); goto out; } if (allocate_some_stuff(&another_pointer) != STUFF_OK) { err("allocate_some_stuff step 2 failed, abort"); goto out; } void *some_temporary_malloc = malloc(1000); //Do something with the data here info("do_something OK"); ret = true; // Assign outputs only on success so we don't end up with // dangling pointers *ptr1 = some_pointer; *ptr2 = another_pointer; out: if (!ret) { //We are returning an error, clean up everything //deallocate_some_stuff is a NO-OP if pointer is NULL deallocate_some_stuff(some_pointer); deallocate_some_stuff(another_pointer); } //this needs to be freed every time free(some_temporary_malloc); return ret; }
我更喜欢使用在下面的例子中描述的技术…
struct lnode *insert(char *data, int len, struct lnode *list) { struct lnode *p, *q; uint8_t good; struct { uint8_t alloc_node : 1; uint8_t alloc_str : 1; } cleanup = { 0, 0 }; // allocate node. p = (struct lnode *)malloc(sizeof(struct lnode)); good = cleanup.alloc_node = (p != NULL); // good? then allocate str if (good) { p->str = (char *)malloc(sizeof(char)*len); good = cleanup.alloc_str = (p->str != NULL); } // good? copy data if(good) { memcpy ( p->str, data, len ); } // still good? insert in list if(good) { if(NULL == list) { p->next = NULL; list = p; } else { q = list; while(q->next != NULL && good) { // duplicate found--not good good = (strcmp(q->str,p->str) != 0); q = q->next; } if (good) { p->next = q->next; q->next = p; } } } // not-good? cleanup. if(!good) { if(cleanup.alloc_str) free(p->str); if(cleanup.alloc_node) free(p); } // good? return list or else return NULL return (good? list: NULL);
}
来源: http : //blog.staila.com/?p=114
我们使用Daynix CSteps
库作为init函数中的“ goto问题 ”的另一个解决scheme。
看到这里和这里 。