有没有更好的方法来进行C风格的error handling?
我正在尝试通过编写一个简单的解析器/编译器来学习C语言。 到目前为止它是一个非常有启发性的经验,但是来自C#的强大背景我调整了一些问题 – 特别是缺乏例外。
现在我已经阅读了Cleaner,更优雅,更难以识别 ,我同意该文章中的每一个字; 在我的C#代码中,我尽可能避免抛出exception,但是现在我面临着一个我无法抛出exception的世界,我的error handling完全淹没了我的代码的干净且易于阅读的逻辑。
目前我正在编写需要在出现问题时快速失败的代码,并且它也可能深度嵌套 – 我已经确定了error handling模式,其中“Get”函数在错误上返回NULL,其他函数返回-1
失败。 在这两种情况下,失败的函数都会调用NS_SetError()
,所以调用函数需要做的就是清理并立即返回失败。
我的问题是if (Action() < 0) return -1;
我所做的陈述正在进行 – 它非常重复,完全掩盖了潜在的逻辑。 我最终创建了一个简单的宏来尝试改善这种情况,例如:
#define NOT_ERROR(X) if ((X) str, "+") == 0) { NOT_ERROR(NS_Add()); } else if (strcmp(current->str, "-") == 0) { NOT_ERROR(NS_Subtract()); } else { NS_SetError("Expected: operator"); return -1; } return 0; }
NS_Term
函数NS_Term
, NS_Add
和NS_Subtract
执行NS_SetError()
并在出现错误时返回-1
– 它更好 ,但它仍然感觉我在滥用宏并且不允许任何清理(某些函数,特别是Get
返回指针的函数,更复杂,需要运行清理代码)。
总的来说,我觉得我错过了一些东西 – 尽管这种方式的error handling应该更容易识别,在我的许多function中,我真的很难确定是否正确处理错误:
- 某些函数在错误时返回
NULL
- 某些函数在出错时返回
< 0
- 某些function永远不会产生错误
- 我的函数执行
NS_SetError()
,但许多其他函数没有。
有没有更好的方法可以构建我的function,还是其他人都有这个问题?
还有Get
函数(返回指向对象的指针)在错误上返回NULL
是一个好主意,还是只是让我的error handling混乱?
一种清理技术是使用一个永远不会实际迭代的while循环。 它让你转到goto而不使用goto。
#define NOT_ERROR(x) if ((x) < 0) break; #define NOT_NULL(x) if ((x) == NULL) break; // Initialise things that may need to be cleaned up here. char* somePtr = NULL; do { NOT_NULL(somePtr = malloc(1024)); NOT_ERROR(something(somePtr)); NOT_ERROR(somethingElse(somePtr)); // etc // if you get here everything's ok. return somePtr; } while (0); // Something went wrong so clean-up. free(somePtr); return NULL;
你失去了一定程度的缩进。
编辑:我想补充一点,我没有反对goto,只是对于提问者的用例,他并不真的需要它。 有些情况下使用goto可以将裤子从任何其他方法中击败,但这不是其中之一。
当你必须在每次从错误return
之前重复相同的最终化代码时,这是一个更大的问题。 在这种情况下,使用goto
被广泛接受:
int func () { if (a() < 0) { goto failure_a; } if (b() < 0) { goto failure_b; } if (c() < 0) { goto failure_c; } return SUCCESS; failure_c: undo_b(); failure_b: undo_a(); failure_a: return FAILURE; }
你甚至可以在这周围创建自己的宏来节省一些打字,就像这样(我还没有测试过):
#define CALL(funcname, ...) \ if (funcname(__VA_ARGS__) < 0) { \ goto failure_ ## funcname; \ }
总的来说,这是一个比简单处理更清洁,更少冗余的方法:
int func () { if (a() < 0) { return FAILURE; } if (b() < 0) { undo_a(); return FAILURE; } if (c() < 0) { undo_b(); undo_a(); return FAILURE; } return SUCCESS; }
作为一个额外的提示,我经常使用链接来减少代码中if
的数量:
if (a() < 0 || b() < 0 || c() < 0) { return FAILURE; }
自||
是一个短路运营商,上面将取代三个单独的if
。 考虑在return
语句中使用链接:
return (a() < 0 || b() < 0 || c() < 0) ? FAILURE : SUCCESS;
你可能不想听到这个,但C方式做exception是通过goto
语句 。 这是它在语言中的原因之一。
另一个原因是goto
是状态机实现的自然表达。 什么常见的编程任务最好由状态机代表? 词法分析器。 看看lex
的输出。 GOTO语句。
因此,对我而言,现在听起来是时候让你对语言语法元素, goto
的语言变得亲密。
这必须至少考虑两个层面:你的function如何相互作用,以及它在中断时的作用。
我看到的大多数大型C框架总是通过引用返回状态和“返回”值(这是WinAPI和许多C Mac OS API的情况)。 你想归还一个布尔?
StatusCode FooBar(int a, int b, int c, bool* output);
你想要返回一个指针?
StatusCode FooBar(int a, int b, int c, char** output);
反正你懂这个意思。
在调用函数方面,我经常看到的模式是使用指向清理标签的goto语句:
if (statusCode < 0) goto error; /* snip */ return everythingWentWell; error: cleanupResources(); return somethingWentWrong;
除了goto
之外,标准C还有另一个构造来处理exception的流量控制setjmp/longjmp
。 它的优势在于,您可以比使用某人提议的break
更容易地break
多个嵌套控制语句,并且除了goto
提供的状态指示之外,还可以编码出错的原因。
另一个问题只是构造的语法。 使用可能无意中添加的控制语句不是一个好主意。 在你的情况下
if (bla) NOT_ERROR(X); else printf("wow!\n");
会发生根本性的错误。 我会用类似的东西
#define NOT_ERROR(X) \ if ((X) >= 0) { (void)0; } \ else return -1
代替。
简短的回答是:让您的函数返回一个不可能是有效值的错误代码 – 并始终检查返回值。 对于返回指针的函数,这是NULL
。 对于返回非负int
函数,它是负值,通常为-1
,依此类推……
如果每个可能的返回值也是有效值 ,请使用call-by-reference:
int my_atoi(const char *str, int *val) { // convert str to int // store the result in *val // return 0 on success, -1 (or any other value except 0) otherwise }
检查每个函数的返回值可能看起来很乏味,但这就是在C中处理错误的方式。考虑函数nc_dial() 。 它所做的就是通过调用getaddrinfo(),socket(),setsockopt(),bind()/ listen()或connect()来检查其参数的有效性并建立网络连接,最后释放未使用的资源并更新元数据。 这可以在大约15行中完成。 但是,由于错误检查,该function有近100行。 但这就是它在C中的方式。一旦你习惯了它,你就可以轻松掩盖你头脑中的错误检查。
此外,多个if (Action() == 0) return -1;
并没有错if (Action() == 0) return -1;
。 恰恰相反:它通常是一个谨慎的程序员的标志。 保持谨慎是件好事。
并且作为最后的评论:如果你不能certificate他们的使用是正当的,而有人用枪瞄准你的头,那么不要使用宏来定义值 。 更具体地说,永远不要在宏中使用控制流语句:它会让那些在你离开公司5年后必须维护代码的穷人感到困惑。 if (foo) return -1;
没有错if (foo) return -1;
。 它简单,干净,明显,你不能做得更好。
一旦你放弃了在宏中隐藏控制流的倾向,就没有理由感到你错过了什么。
goto语句是实现exception样式处理的最简单且最可能最干净的方法。 如果在宏args中包含比较逻辑,则使用宏可以更容易地读取。 如果您组织例程来执行正常(即非错误)工作并且仅使用goto on exceptions,那么它的读取相当干净。 例如:
/* Exception macro */ #define TRY_EXIT(Cmd) { if (!(Cmd)) {goto EXIT;} } /* My memory allocator */ char * MyAlloc(int bytes) { char * pMem = NULL; /* Must have a size */ TRY_EXIT( bytes > 0 ); /* Allocation must succeed */ pMem = (char *)malloc(bytes); TRY_EXIT( pMem != NULL ); /* Initialize memory */ TRY_EXIT( initializeMem(pMem, bytes) != -1 ); /* Success */ return (pMem); EXIT: /* Exception: Cleanup and fail */ if (pMem != NULL) free(pMem); return (NULL); }
那这个呢?
int NS_Expression(void) { int ok = 1; ok = ok && NS_Term(); ok = ok && Emit("MOVE D0, D1\n"); ok = ok && NS_AddSub(); return ok }
我从来没有想过以这种方式使用goto
或do { } while(0)
进行error handling – 它非常整洁,但是在考虑之后我意识到在很多情况下我可以通过将函数分成多个来做同样的事情二:
int Foo(void) { // Initialise things that may need to be cleaned up here. char* somePtr = malloc(1024); if (somePtr = NULL) { return NULL; } if (FooInner(somePtr) < 0) { // Something went wrong so clean-up. free(somePtr); return NULL; } return somePtr; } int FooInner(char* somePtr) { if (something(somePtr) < 0) return -1; if (somethingElse(somePtr) < 0) return -1; // etc // if you get here everything's ok. return 0; }
这现在意味着你获得了额外的function,但我仍然倾向于许多短function。
在飞利浦的建议之后,我也决定避免使用控制流宏 - 只要你把它们放在一条线上就足够清楚了。
至少它让人放心,我知道我不仅仅缺少一些东西 - 其他人也有这个问题! 🙂
使用setjmp 。
http://en.wikipedia.org/wiki/Setjmp.h
http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html
http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html
#include #include jmp_buf x; void f() { longjmp(x,5); // throw 5; } int main() { // output of this program is 5. int i = 0; if ( (i = setjmp(x)) == 0 )// try{ { f(); } // } --> end of try{ else // catch(i){ { switch( i ) { case 1: case 2: default: fprintf( stdout, "error code = %d\n", i); break; } } // } --> end of catch(i){ return 0; }
#include #include #define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){ #define CATCH } else { #define ETRY } }while(0) #define THROW longjmp(ex_buf__, 1) int main(int argc, char** argv) { TRY { printf("In Try Statement\n"); THROW; printf("I do not appear\n"); } CATCH { printf("Got Exception!\n"); } ETRY; return 0; }