在C库中以一致的方式处理错误处理错误时,您认为"最佳实践"是什么?
我一直在考虑两种方式:
始终返回错误代码.典型的功能如下所示:
MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);
始终提供错误指针方法:
int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);
使用第一种方法时,可以编写这样的代码,其中错误处理检查直接放在函数调用上:
int size; if(getObjectSize(h, &size) != MYAPI_SUCCESS) { // Error handling }
这看起来比错误处理代码更好.
MYAPIError error; int size; size = getObjectSize(h, &error); if(error != MYAPI_SUCCESS) { // Error handling }
但是,我认为使用返回值返回数据会使代码更具可读性.很明显,在第二个示例中,某些内容被写入了size变量.
您对我为什么应该选择这些方法或者将它们混合或使用其他方法有任何想法吗?我不是全局错误状态的粉丝,因为它往往会使库的多线程使用更加痛苦.
编辑:只要他们不涉及异常,C++关于此的具体想法也会很有趣,因为目前我不能选择...
我已经使用了这两种方法,它们对我来说都很好.无论我使用哪一个,我总是尝试应用这个原则:
如果唯一可能的错误是程序员错误,请不要返回错误代码,在函数内部使用断言.
验证输入的断言清楚地传达了函数所期望的内容,而过多的错误检查会使程序逻辑模糊不清.决定如何处理所有各种错误情况可能会使设计复杂化.为什么要弄清楚函数X应该如何处理空指针,如果你可以坚持程序员永远不会传递一个?
我喜欢错误作为返回值方式.如果您正在设计api并且想要尽可能轻松地使用您的库,请考虑以下添加:
将所有可能的错误状态存储在一个typedef'ed枚举中,并在lib中使用它.不要只返回整数或更糟,混合整数或不同的枚举与返回代码.
提供将错误转换为人类可读的东西的功能.可以很简单.只是错误枚举,const char*out.
我知道这个想法使多线程使用有点困难,但如果应用程序员可以设置全局错误回调那就太好了.这样他们就可以在bug追捕会话期间将断点放入回调中.
希望能帮助到你.
CMU的CERT 有一套很好的幻灯片,建议何时使用每种常见的C(和C++)错误处理技术.最佳幻灯片之一是这个决策树:
我会亲自改变关于这款跑车的两件事.
首先,我要澄清有时对象应该使用返回值来指示错误.如果函数仅从对象中提取数据但不改变对象,则对象本身的完整性不存在风险,并且使用返回值指示错误更合适.
其次,在C++中使用异常并不总是合适的.异常是好的,因为它们可以减少用于错误处理的源代码量,它们通常不会影响函数签名,并且它们可以非常灵活地传递callstack的数据.另一方面,由于以下几个原因,异常可能不是正确的选择:
C++异常具有非常特殊的语义.如果你不想要那些语义,那么C++异常是一个糟糕的选择.抛出后必须立即处理异常,并且设计有利于错误需要将调用堆栈展开几个级别.
抛出异常的C++函数以后不会被包装成不抛出异常,至少在没有支付异常的全部代价的情况下也是如此.可以包含返回错误代码的函数以抛出C++异常,从而使它们更加灵活.C++ new
通过提供非投掷变体来实现这一目标.
C++异常相对较为昂贵,但这种缺点主要是对于合理使用异常的程序而言过于夸张.程序根本不应该在性能受到关注的代码路径上抛出异常.程序报告错误和退出的速度并不重要.
有时C++异常不可用.要么它们在一个人的C++实现中根本不可用,要么一个代码指南禁止它们.
由于最初的问题是关于多线程的上下文,我认为本地错误指示器技术(在SirDarius的答案中描述的内容)在原始答案中被低估了.它是线程安全的,不会强制错误被调用者立即处理,并且可以捆绑描述错误的任意数据.缺点是它必须由一个对象保持(或者我想以某种方式与外部相关联)并且可以说比返回代码更容易被忽略.
每当我创建一个库时,我都会使用第一种方法.使用typedef'ed枚举作为返回码有几个优点.
如果函数返回一个更复杂的输出,例如数组和它的长度,则不需要创建任意结构来返回.
rc = func(..., int **return_array, size_t *array_length);
它允许简单,标准化的错误处理.
if ((rc = func(...)) != API_SUCCESS) { /* Error Handling */ }
它允许在库函数中进行简单的错误处理.
/* Check for valid arguments */ if (NULL == return_array || NULL == array_length) return API_INVALID_ARGS;
使用typedef'ed枚举还允许枚举名称在调试器中可见.这样可以更轻松地进行调试,而无需经常查阅头文件.具有将此枚举转换为字符串的功能也很有用.
无论采用何种方法,最重要的问题是保持一致.这适用于函数和参数命名,参数排序和错误处理.
使用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; }
我个人更喜欢前一种方法(返回错误指示器).
必要时,返回结果应该只表示发生了错误,另一个函数用于找出确切的错误.
在你的getSize()示例中,我认为大小必须始终为零或正数,因此返回否定结果可能表示错误,就像UNIX系统调用一样.
我想不出我使用过的任何库,后者使用作为指针传入的错误对象. stdio
等都带有返回值.
当我编写程序时,在初始化期间,我通常会分离一个线程进行错误处理,并初始化一个特殊的错误结构,包括一个锁.然后,当我通过返回值检测到错误时,我将异常的信息输入到结构中并将SIGIO发送到异常处理线程,然后查看是否无法继续执行.如果我不能,我将一个SIGURG发送到异常线程,它会正常地停止程序.
我过去做了很多C编程.而且我真的贬低了错误代码的返回值.但是有几个可能的陷阱:
重复的错误号,可以使用全局errors.h文件解决.
忘记检查错误代码,这应该通过cluebat和长调试时间来解决.但最后你会学到(或者你会知道别人会做调试).
UNIX方法与您的第二个建议最相似.返回结果或单个"它出错"值.例如,open将在成功时返回文件描述符,或在失败时返回-1.失败时,它还会设置errno
一个外部全局整数来指示发生了哪个故障.
对于它的价值,Cocoa也采用了类似的方法.许多方法返回BOOL,并接受一个NSError **
参数,因此失败时它们会设置错误并返回NO.然后错误处理如下:
NSError *error = nil; if ([myThing doThingError: &error] == NO) { // error handling }
这是你的两个选项之间的某个地方:-).
返回错误代码是C中错误处理的常用方法.
但是最近我们也尝试了传出的错误指针方法.
它比返回值方法有一些优势:
您可以使用返回值来实现更有意义的目的.
必须写出该错误参数会提醒您处理错误或传播错误.(你永远不会忘记检查返回值fclose
,不是吗?)
如果使用错误指针,则可以在调用函数时将其传递下去.如果任何函数设置它,该值将不会丢失.
通过在错误变量上设置数据断点,您可以先捕获错误发生的位置.通过设置条件断点,您也可以捕获特定的错误.
无论您是否处理所有错误,都可以更轻松地自动化检查.代码约定可能会强制您调用错误指针err
,它必须是最后一个参数.所以脚本可以匹配字符串err);
然后检查它是否跟着if (*err
.实际上在实践中我们制作了一个名为CER
(check err return)和CEG
(check err goto)的宏.因此,当我们只是想要返回错误时,您不需要输出它,并且可以减少视觉混乱.
并非我们代码中的所有函数都具有此传出参数.此传出参数用于通常抛出异常的情况.
这是一种我认为有趣的方法,同时需要一些纪律.
这假设handle-type变量是操作所有API函数的实例.
这个想法是句柄后面的结构将前一个错误存储为具有必要数据(代码,消息...)的结构,并且为用户提供了一个返回指针tp这个错误对象的函数.每个操作都将更新指向的对象,以便用户无需调用函数即可检查其状态.与errno模式相反,错误代码不是全局的,只要每个句柄都被正确使用,这使得该方法是线程安全的.
例:
MyHandle * h = MyApiCreateHandle(); /* first call checks for pointer nullity, since we cannot retrieve error code on a NULL pointer */ if (h == NULL) return 0; /* from here h is a valid handle */ /* get a pointer to the error struct that will be updated with each call */ MyApiError * err = MyApiGetError(h); MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext"); /* we want to know what can go wrong */ if (err->code != MyApi_ERROR_OK) { fprintf(stderr, "(%d) %s\n", err->code, err->message); MyApiDestroy(h); return 0; } MyApiRecord record; /* here the API could refuse to execute the operation if the previous one yielded an error, and eventually close the file descriptor itself if the error is not recoverable */ MyApiReadFileRecord(h, &record, sizeof(record)); /* we want to know what can go wrong, here using a macro checking for failure */ if (MyApi_FAILED(err)) { fprintf(stderr, "(%d) %s\n", err->code, err->message); MyApiDestroy(h); return 0; }