C代码中的错误处理

Las*_*lan 138 c error-handling

在C库中以一致的方式处理错误处理错误时,您认为"最佳实践"是什么?

我一直在考虑两种方式:

始终返回错误代码.典型的功能如下所示:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);
Run Code Online (Sandbox Code Playgroud)

始终提供错误指针方法:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);
Run Code Online (Sandbox Code Playgroud)

使用第一种方法时,可以编写这样的代码,其中错误处理检查直接放在函数调用上:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}
Run Code Online (Sandbox Code Playgroud)

这看起来比错误处理代码更好.

MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
    // Error handling
}
Run Code Online (Sandbox Code Playgroud)

但是,我认为使用返回值返回数据会使代码更具可读性.很明显,在第二个示例中,某些内容被写入了size变量.

您对我为什么应该选择这些方法或者将它们混合或使用其他方法有任何想法吗?我不是全局错误状态的粉丝,因为它往往会使库的多线程使用更加痛苦.

编辑:只要他们不涉及异常,C++关于此的具体想法也会很有趣,因为目前我不能选择...

ASh*_*lly 87

我已经使用了这两种方法,它们对我来说都很好.无论我使用哪一个,我总是尝试应用这个原则:

如果唯一可能的错误是程序员错误,请不要返回错误代码,在函数内部使用断言.

验证输入的断言清楚地传达了函数所期望的内容,而过多的错误检查会使程序逻辑模糊不清.决定如何处理所有各种错误情况可能会使设计复杂化.为什么要弄清楚函数X应该如何处理空指针,如果你可以坚持程序员永远不会传递一个?

  • 呃,**绝对不会在库代码中使用断言**!另外,[不要在一段代码中混合使用各种类型的错误处理](http://blog.existentialize.com/the-story-of-the-gnutls-bug.html)就像其他人一样...... (11认同)
  • 我当然同意不混合风格.我很好奇你对断言的推理.如果我的函数文档说"参数X必须不是NULL"或"Y必须是这个枚举的成员",那么`assert(X!= NULL);`或`assert(Y <enumtype_MAX); ?请参阅[程序员的这个答案](http://programmers.stackexchange.com/a/65031/145)及其链接的问题,了解我认为这是正确的方法的更多细节. (8认同)
  • @AShelly声明他们通常不在发布版本中的问题. (7认同)

Nil*_*nck 65

我喜欢错误作为返回值方式.如果您正在设计api并且想要尽可能轻松地使用您的库,请考虑以下添加:

  • 将所有可能的错误状态存储在一个typedef'ed枚举中,并在lib中使用它.不要只返回整数或更糟,混合整数或不同的枚举与返回代码.

  • 提供将错误转换为人类可读的东西的功能.可以很简单.只是错误枚举,const char*out.

  • 我知道这个想法使多线程使用有点困难,但如果应用程序员可以设置全局错误回调那就太好了.这样他们就可以在bug追捕会话期间将断点放入回调中.

希望能帮助到你.

  • 如果您想要传达错误的更多细节怎么办?例如,您有一个解析器错误,并希望提供语法错误的行号和列以及一种很好地打印它的方法. (7认同)
  • 为什么这么说,"这个想法使得多线程使用有点困难." 通过多线程使哪个部分变得困难?你能举个简单的例子吗? (4认同)
  • @crypticcoder 简单地说:可以在任何线程上下文中调用全局错误回调。如果您只是打印出错误,您将不会遇到任何问题。如果您尝试更正问题,则必须找出导致错误的调用线程,这使事情变得困难。 (2认同)

Pra*_*tic 22

CMU的CERT 有一套很好的幻灯片,建议何时使用每种常见的C(和C++)错误处理技术.最佳幻灯片之一是这个决策树:

错误处理决策树

我会亲自改变关于这款跑车的两件事.

首先,我要澄清有时对象应该使用返回值来指示错误.如果函数仅从对象中提取数据但不改变对象,则对象本身的完整性不存在风险,并且使用返回值指示错误更合适.

其次,在C++中使用异常并不总是合适的.异常是好的,因为它们可以减少用于错误处理的源代码量,它们通常不会影响函数签名,并且它们可以非常灵活地传递callstack的数据.另一方面,由于以下几个原因,异常可能不是正确的选择:

  1. C++异常具有非常特殊的语义.如果你不想要那些语义,那么C++异常是一个糟糕的选择.抛出后必须立即处理异常,并且设计有利于错误需要将调用堆栈展开几个级别.

  2. 抛出异常的C++函数以后不会被包装成不抛出异常,至少在没有支付异常的全部代价的情况下也是如此.可以包含返回错误代码的函数以抛出C++异常,从而使它们更加灵活.C++ new通过提供非投掷变体来实现这一目标.

  3. C++异常相对较为昂贵,但这种缺点主要是对于合理使用异常的程序而言过于夸张.程序根本不应该在性能受到关注的代码路径上抛出异常.程序报告错误和退出的速度并不重要.

  4. 有时C++异常不可用.要么它们在一个人的C++实现中根本不可用,要么一个代码指南禁止它们.


由于最初的问题是关于多线程的上下文,我认为本地错误指示器技术(在SirDarius答案中描述的内容)在原始答案中被低估了.它是线程安全的,不会强制错误被调用者立即处理,并且可以捆绑描述错误的任意数据.缺点是它必须由一个对象保持(或者我想以某种方式与外部相关联)并且可以说比返回代码更容易被忽略.

  • 您可能会注意到Google的C++编码标准仍然说[我们不使用C++例外.](https://google.github.io/styleguide/cppguide.html#Exceptions) (3认同)

小智 17

每当我创建一个库时,我都会使用第一种方法.使用typedef'ed枚举作为返回码有几个优点.

  • 如果函数返回一个更复杂的输出,例如数组和它的长度,则不需要创建任意结构来返回.

    rc = func(..., int **return_array, size_t *array_length);
    
    Run Code Online (Sandbox Code Playgroud)
  • 它允许简单,标准化的错误处理.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 它允许在库函数中进行简单的错误处理.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
    
    Run Code Online (Sandbox Code Playgroud)
  • 使用typedef'ed枚举还允许枚举名称在调试器中可见.这样可以更轻松地进行调试,而无需经常查阅头文件.具有将此枚举转换为字符串的功能也很有用.

无论采用何种方法,最重要的问题是保持一致.这适用于函数和参数命名,参数排序和错误处理.


Ami*_*yan 9

使用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 <setjmp.h>
#include <stdio.h>

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;
}
Run Code Online (Sandbox Code Playgroud)
#include <stdio.h>
#include <setjmp.h>

#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;
}
Run Code Online (Sandbox Code Playgroud)

  • 第二个代码块基于答案顶部引用的[Francesco Nidito的页面](http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html)中的代码的早期版本.自写这篇答案以来,"ETRY"代码已被修改. (2认同)
  • Setjmp 是一种可怕的错误处理策略。如果您在 setjmp 和 longjmp 调用之间分配任何内容,它很昂贵,容易出错(带有非易失性更改的本地变量不保留其更改的值等等)并且会泄漏资源。在收回 sigjmp/longjmp 的成本之前,您应该能够进行 30 次返回和 int-val 检查。大多数调用堆栈不会深入到那么深,特别是如果您不重视递归(如果这样做,除了返回+检查的成本之外,您还有性能问题)。 (2认同)
  • 如果您分配内存然后抛出,内存将永远泄漏。另外,“setjmp”的成本很高,即使没有抛出任何错误,它也会消耗大量的 CPU 时间和堆栈空间。在 Windows 上使用 gcc 时,您可以在不同的 C++ 异常处理方法之间进行选择,其中一种基于“setjmp”,它会使您的代码在实践中减慢 30%。 (2认同)

Aln*_*tak 7

我个人更喜欢前一种方法(返回错误指示器).

必要时,返回结果应该只表示发生了错误,另一个函数用于找出确切的错误.

在你的getSize()示例中,我认为大小必须始终为零或正数,因此返回否定结果可能表示错误,就像UNIX系统调用一样.

我想不出我使用过的任何库,后者使用作为指针传入的错误对象. stdio等都带有返回值.


小智 7

当我编写程序时,在初始化期间,我通常会分离一个线程进行错误处理,并初始化一个特殊的错误结构,包括一个锁.然后,当我通过返回值检测到错误时,我将异常的信息输入到结构中并将SIGIO发送到异常处理线程,然后查看是否无法继续执行.如果我不能,我将一个SIGURG发送到异常线程,它会正常地停止程序.


Gab*_*les 7

这是一个简单的程序,用于演示Nils Pipenbrinck 的答案的前 2 个要点

他的前两颗子弹是:

  • 将所有可能的错误状态存储在一个 typedef 的枚举中,并在您的库中使用它。不要只返回整数,甚至更糟,不要将整数或不同的枚举与返回码混合在一起。

  • 提供一个将错误转换为人类可读的函数。可以很简单。只是错误枚举输入,const char* 输出。

假设您编写了一个名为mymodule. 首先,在 mymodule.h 中,定义基于枚举的错误代码,并编写一些与这些代码对应的错误字符串。在这里,我使用了一个 C 字符串数组 ( char *),它仅在您的第一个基于枚举的错误代码的值为 0 时才有效,并且此后您不操作这些数字。如果您确实使用带有间隙或其他起始值的错误代码编号,您只需从使用映射的 C 字符串数组(如下所示)更改为使用使用 switch 语句或 if / else if 语句的函数从枚举错误代码映射到可打印的 C 字符串(我没有演示)。这是你的选择。

我的模块.h

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);
Run Code Online (Sandbox Code Playgroud)

mymodule.c 包含我的映射函数,用于从枚举错误代码映射到可打印的 C 字符串:

我的模块

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}
Run Code Online (Sandbox Code Playgroud)

main.c 包含一个测试程序来演示调用一些函数并从中打印一些错误代码:

主文件

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出:

演示
来自 mymodule_func1() 的C(或 C++)错误代码中基于枚举的错误代码= MYMODULE_ERROR_OK
来自 mymodule_func2() 的
错误代码= MYMODULE_ERROR_INVARG来自 mymodule_func3() 的错误代码 = MYMODULE_ERROR_MYERROR

参考:

您可以在此处自己运行此代码:https : //onlinegdb.com/ByEbKLupS


Too*_*the 6

我过去做了很多C编程.而且我真的贬低了错误代码的返回值.但是有几个可能的陷阱:

  • 重复的错误号,可以使用全局errors.h文件解决.
  • 忘记检查错误代码,这应该通过cluebat和长调试时间来解决.但最后你会学到(或者你会知道别人会做调试).

  • 第二个问题可以通过适当的编译器警告级别、适当的代码审查机制和静态代码分析器工具来解决。 (2认同)

小智 6

UNIX方法与您的第二个建议最相似.返回结果或单个"它出错"值.例如,open将在成功时返回文件描述符,或在失败时返回-1.失败时,它还会设置errno一个外部全局整数来指示发生了哪个故障.

对于它的价值,Cocoa也采用了类似的方法.许多方法返回BOOL,并接受一个NSError **参数,因此失败时它们会设置错误并返回NO.然后错误处理如下:

NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
  // error handling
}
Run Code Online (Sandbox Code Playgroud)

这是你的两个选项之间的某个地方:-).


Cal*_*ius 6

返回错误代码是C中错误处理的常用方法.

但是最近我们也尝试了传出的错误指针方法.

它比返回值方法有一些优势:

  • 您可以使用返回值来实现更有意义的目的.

  • 必须写出该错误参数会提醒您处理错误或传播错误.(你永远不会忘记检查返回值fclose,不是吗?)

  • 如果使用错误指针,则可以在调用函数时将其传递下去.如果任何函数设置它,该值将不会丢失.

  • 通过在错误变量上设置数据断点,您可以先捕获错误发生的位置.通过设置条件断点,您也可以捕获特定的错误.

  • 无论您是否处理所有错误,都可以更轻松地自动化检查.代码约定可能会强制您调用错误指针err,它必须是最后一个参数.所以脚本可以匹配字符串err);然后检查它是否跟着if (*err.实际上在实践中我们制作了一个名为CER(check err return)和CEG(check err goto)的宏.因此,当我们只是想要返回错误时,您不需要输出它,并且可以减少视觉混乱.

并非我们代码中的所有函数都具有此传出参数.此传出参数用于通常抛出异常的情况.


All*_*ind 6

我多次遇到这个问答,并想提供更全面的答案。我认为考虑这个问题的最好方法是如何向调用者返回错误,以及返回什么。

如何

从函数返回信息有 3 种方法:

  1. 返回值
  2. 输出参数
  3. 带外,包括非本地 goto (setjmp/longjmp)、文件或全局范围变量、文件系统等。

返回值

只能返回单个值(对象);但是,它可以是任意复杂的值。这是错误返回函数的示例:

  enum error hold_my_beer(void);
Run Code Online (Sandbox Code Playgroud)

返回值的好处之一是它允许链接调用以减少侵入性错误处理:

  !hold_my_beer() &&
  !hold_my_cigarette() &&
  !hold_my_pants() ||
  abort();
Run Code Online (Sandbox Code Playgroud)

这不仅关系到可读性,而且还可以允许以统一的方式处理此类函数指针的数组。

输出参数

您可以通过参数通过多个对象返回更多内容,但最佳实践确实建议保持较低的参数总数(例如,<=4):

void look_ma(enum error *e, char *what_broke);

enum error e;
look_ma(e);
if(e == FURNITURE) {
  reorder(what_broke);
} else if(e == SELF) {
  tell_doctor(what_broke);
}
Run Code Online (Sandbox Code Playgroud)

这会强制调用者传入对象,这可能会使其更有可能被检查。如果您有一组调用全部返回错误,并且您决定为每个调用分配一个新变量,那么它会给调用者带来一些混乱。

带外

最著名的例子可能是(线程本地)errno 变量,它是被调用函数设置的。被调用者很容易不检查此变量,并且您只会得到一个,如果您的函数很复杂(例如,函数的两个部分返回相同的错误代码),这可能会成为问题。

使用 setjmp() 可以定义一个位置以及如何处理 int 值,然后通过 longjmp() 将控制权转移到该位置。请参阅C 中 setjmp 和 longjmp 的实际用法

什么

  1. 指标
  2. 代码
  3. 目的
  4. 打回来

指标

错误指示器仅告诉您存在问题,但不告诉您该问题的性质:

struct foo *f = foo_init();
if(!f) {
  /// handle the absence of foo
}
Run Code Online (Sandbox Code Playgroud)

这是函数传达错误状态的最弱的方式;然而,如果调用者无论如何都无法以渐进的方式响应错误,那就完美了。

代码

错误代码告诉调用者问题的性质,并可能允许适当的响应(来自上面)。它可以是一个返回值,或者像上面的错误参数的look_ma()示例一样。

目的

通过错误对象,调用者可以了解任意复杂的问题。例如,错误代码和合适的人类可读消息。它还可以通知调用者处理集合时出现了多个问题,或者每个项目出现错误:

struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
   if(reason[i] == NOT_FOUND) find(friends[i]);
}
Run Code Online (Sandbox Code Playgroud)

当然,您也可以根据需要动态(重新)分配它,而不是预先分配错误数组。

打回来

回调是处理错误的最强大方法,因为您可以告诉函数您希望看到出现问题时发生什么行为。可以将回调参数添加到每个函数中,或者如果仅需要每个结构实例进行自定义用户界面,如下所示:

 struct foo {
    ...
    void (error_handler)(char *);
 };

 void default_error_handler(char *message) { 
    assert(f);
    printf("%s", message);
 }

 void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
    assert(f);
    f->error_handler = eh;
 }

 struct foo *foo_init() {
    struct foo *f = malloc(sizeof(struct foo));
    foo_set_error_handler(f, default_error_handler);
    return f;
 }


 struct foo *f = foo_init();
 foo_something();
Run Code Online (Sandbox Code Playgroud)

回调的一个有趣的好处是,它可以被调用多次,或者在没有错误的情况下根本不被调用,在快乐路径上没有开销。

然而,存在控制反转。调用代码不知道回调是否被调用。因此,使用指标也可能有意义。


naa*_*ing 5

我最近也在思考这个问题,并为 C编写了一些宏,它们使用纯本地返回值模拟 try-catch-finally 语义。希望你觉得它有用。


Sir*_*ius 5

这是一种我认为有趣的方法,同时需要一些纪律.

这假设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;
}
Run Code Online (Sandbox Code Playgroud)