【问题标题】:Any good idioms for error handling in straight C programs?在直接 C 程序中处理错误有什么好的习惯用法吗?
【发布时间】:2011-02-16 21:55:42
【问题描述】:

重新开始一些 C 工作。

我的许多函数如下所示:

int err = do_something(arg1, arg2, arg3, &result);

根据意图,函数会填充结果,返回值是调用的状态。

黑暗面是你得到了这样的天真:

int err = func1(...);
if (!err) {
    err = func2(...);
    if (!err) {
        err = func3(...);
    }
}
return err;

我想我可以宏化它:

#define ERR(x) if (!err) { err = (x) }
int err = 0;
ERR(func1(...));
ERR(func2(...));
ERR(func3(...));
return err;

但这只有在我链接函数调用而不是做其他工作时才有效。

很明显,Java、C#、C++ 有例外,它们非常适合这类事情。

我只是好奇现在其他人在做什么以及其他人如何在他们的 C 程序中进行错误处理。

【问题讨论】:

  • 您可以通过检查err 是否为非零并清理/返回来避免这种嵌套。我觉得这样会更好看。
  • 我给出了用于不同问题的解决方案:stackoverflow.com/questions/1073397/…
  • @Makis - 如果您只关心错误的存在而不是实际的结果代码,该技术就可以正常工作。在我的案例中,我会对结果代码感兴趣。不过谢谢!
  • 有一种“风格”使用 C++,就好像它是“C with exceptions”一样。我以前见过。我遇到的麻烦不在于错误检查的线性级联,而是我的错误检查在几个函数层内。对于“确保你现在离开这里”这件事,异常处理是无可匹敌的。
  • @Warren P:因为这样的问题,我考虑过 C++,但我会等待。作为推论,我在 C 中看到了使用 longjmp 的异常机制(我实际上已经使用过它们,但那是几年前的事了)。

标签: c error-handling


【解决方案1】:

如果你最后有资源需要释放,那么有时候老可信赖的goto可以派上用场!

int
major_func(size_t len)
{
    int err;
    char *buf;

    buf = malloc(len);

    if (err = minor_func1(buf))
        goto major_func_end;
    if (err = minor_func2(buf))
        goto major_func_end;
    if (err = minor_func3(buf))
        goto major_func_end;

major_func_end:
    free(buf);
    return err;
}

【讨论】:

  • +1 用于处理资源清理的现实世界
  • 我浏览过的许多开源代码库都使用了“goto to resource collection on err”的习语。这是另一个受到诽谤的指令闪耀的地方。
  • 这可以在没有 goto 的情况下使用 return(CleanupOnError(err, buf)); (该方法包含您在 goto 中的代码)。除了避免 goto 之外,有时您还可以从多个处理函数中使用相同的清理函数(当然,只要它们需要相同的清理)
  • @Jason Williams:你可以编写一个没有控制流的整个程序,但这也不是一个好主意。在这种情况下使用 return 并没有那么糟糕,但没有理由不在这里使用 goto
  • +1 表示积极使用goto。根据当时需要清理多少(以相反的顺序),再举一个带有多个 goto 标签的示例怎么样?我一直用那个成语。
【解决方案2】:

两种典型模式:

int major_func()
{
    int err = 0;

    if (err = minor_func1()) return err;
    if (err = minor_func2()) return err;
    if (err = minor_func3()) return err;

    return 0;
}

int other_idea()
{
    int err = minor_func1();
    if (!err)
        err = minor_func2();
    if (!err)
        err = minor_func3();
    return err;            
}

void main_func()
{
    int err = major_func();
    if (err)
    {
        show_err();
        return;
    }
    happy_happy_joy_joy();

    err = other_idea();
    if (err)
    {
        show_err();
        return;
    }
    happy_happy_joy_joy();
}

【讨论】:

  • +1:我个人会选择major_func()中显示的实现。许多人讨厌编写具有多个返回语句的代码,但这是局部性原则应该覆盖这些问题的一种情况。 PS。 happy_happy_joy_joy() 的实现在哪里?
  • @torak 我认为人们普遍认为happy_happy_joy_joy() 的实现留给读者作为练习。
  • else 之前的happy_... 似乎很肤浅。
  • other_idea 最好在必须在返回之前释放资源时(例如 malloc/free 或 mutex lock/unlock)。否则,我也更喜欢major_func 。但是,我必须承认我有时会求助于major_func 加上goto。 ;)
  • @Judge Maygarden- 我经常做同样的事情。这是 goto 声明合适的罕见情况之一。
【解决方案3】:

你在else 语句中做了什么?如果没有,试试这个:

int err = func1(...);
if (err) {
    return err;
}

err = func2(...);
if (err) {
    return err;
}

err = func3(...);

return err;

这样你就可以将整个函数短路,甚至不用担心下面的函数调用。

编辑

回去再读一遍,我意识到你在else 语句中做了什么并不重要。那种代码可以很容易地在if 块之后立即运行。

【讨论】:

  • return 放入宏中会杀死下一个必须调试您的代码的人...并不是说我自己从来没有做过 :)
  • 是的。我考虑了一下并删除了答案的那一部分,因为我认为我认为这也是一个坏主意:)
【解决方案4】:

如果错误代码是布尔值,请尝试以下更简单的代码:

return func1() && func2() && func3()

【讨论】:

  • +1 用于使用&& 的短路行为。这可以很容易地扩展到非布尔情况。
【解决方案5】:

OpenGL 采用的一种方法是根本不从函数返回错误,而是呈现可以在函数调用后检查的错误状态。这种方法的一个好处是,当您有一个函数实际上想要返回错误代码以外的内容时,您可以以相同的方式处理错误。另一个很好的地方是,如果用户想要调用多个函数并且只有在所有函数都成功的情况下才能成功,那么您可以在调用 x 次后检查错误。

/* call a number of functions which may error.. */
glMatrixMode(GL_MODELVIEW);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnable(GL_TEXTURE_2D);

/* ...check for errors */
if ((error = glGetError()) != GL_NO_ERROR) {
    if (error == GL_INVALID_VALUE)
        printf("error: invalid value creating view");
    else if (error == GL_INVALID_OPERATION)
        printf("error: invalid operation creating view");
    else if (error == GL_OUT_OF_MEMORY)
        printf("error: out of memory creating view");
}

【讨论】:

  • 这很好。您仍然有检查错误代码的负担,但您可以随心所欲地进行检查。我想唯一的原则是不要重置错误状态,除非在 glGetError 函数中。
  • 顺便说一句,应该有glStrError(error)(类似于strerror(errno))而不是if/else/if/else print。或glPrintError("additional message")(类似于perror())。
  • @Will,这正是openGl所做的——“当发生错误时,错误标志被设置为适当的错误代码值。在调用glGetError之前不会记录其他错误,错误代码是返回,并将标志重置为 GL_NO_ERROR。"
  • @J.F. Sebastian +1,但为简单起见,可能不会遇到错误字符串和所有可能以这个可爱的错误代码 LUA_ERRERR: 运行错误处理函数时出现错误的分配。
  • -1 用于推荐全局状态/全局变量。如果你的 API 有一个对象/上下文结构来保持错误状态,那么这种方法很棒。但是如果你不得不求助于一个全局变量,你就会遇到各种各样的噩梦,包括:(1) 线程安全,(2) 当你试图使它成为线程安全时的性能/可移植性,(3) 问题动态加载的模块、全局变量以及可能相关的内存泄漏,......
【解决方案6】:

其他人提出了好主意。这是我见过的成语

int err;
...
err = foo(...);
if (err)
    return err;
...

您可以将其宏化为类似

#define dERR int err=0
#define CALL err = 
#define CHECK do { if (err) return err } while(0)
...
void my_func(void) {
   dERR;
   ...
   CALL foo(...);
   CHECK;

或者,如果你真的有动力,可以使用 CALL 和 CHECK 来使用它们

CALL foo(...) CHECK;

CALL( foo(...) );

--

通常,需要在退出时进行清理的函数(例如空闲内存)是这样编写的:

int do_something_complicated(...) {
    ...

    err = first_thing();
    if (err)
       goto err_out;

    buffer = malloc(...);
    if (buffer == NULL)
        goto err_out

    err = another_complicated(...);
    if (err)
        goto err_out_free;

    ...

   err_out_free:
    free(buffer);
   err_out:
    return err; /* err might be zero */
}

您可以使用该模式,或者尝试使用宏来简化它。

--

最后,如果你感觉/真的/有动力,你可以使用 setjmp/longjmp。

int main(int argc, char *argv[]) {
    jmp_buf on_error;
    int err;
    if (err = setjmp(on_error)) {
        /* error occurred, error code in err */
        return 1;
    } else {
        actual_code(..., on_error);
        return 0;
    }
}
void actual_code(..., jmp_buf on_error) {
    ...
    if (err)
        longjmp(on_error, err);
}

本质上,一个新的 jmp_buf 和一个 setjmp 函数的声明作为设置一个 try 块。 setjmp 返回非零的情况是你的捕获,调用 longjmp 是你的投掷。我通过传递 jmp_buf 来编写此代码,以防您需要嵌套处理程序(例如,如果您需要在发出错误信号之前释放东西);如果您不需要,请随意将 err 和 jmp_buf 声明为全局变量。

或者,您可以使用宏来简单地传递参数。我建议 Perl 的实现方式:

#define pERR jmp_buf _err_handler
#define aERR _err_handler
#define HANDLE_ERRORS do { jmp_buf _err_handler; int err = setjmp(_err_handler);
#define END_HANDLE while(0)
#define TRY if (! err)
#define CATCH else
#define THROW(e) longjmp(_err_handler, e)

void always_fails(pERR, int other_arg) {
    THROW(42);
}
void does_some_stuff(pERR) {
    normal_call(aERR);
    HANDLE_ERRORS
      TRY {
        always_fails(aERR, 23);
      } CATCH {
        /* err is 42 */
      }
    END_HANDLE;
}
int main(int argc, char *argv[]) {
    HANDLE_ERRORS
      TRY {
        does_some_stuff(aERR);
        return 0;
      } CATCH {
        return err;
      }
    DONE_ERRORS;
}

--

呸。我受够了。 (未经测试的疯狂示例。一些细节可能会被关闭。)

【讨论】:

  • +1 用于提及 longjmp。这在较旧的库中似乎很常见,libpng 就是一个例子。我从来没有发现这种方法非常直观,但是当需要在错误时释放资源时它很有效。
【解决方案7】:

你应该看看 DirectX 对 HRESULT 做了什么——基本上就是这样。异常的产生是有原因的。或者,如果您在 Win32 上运行,它们具有在 C 程序中运行的 SEH。

【讨论】:

    【解决方案8】:

    现在来点完全不同的……

    另一种方法是使用结构来包含您的错误信息,例如:

    struct ErrorInfo
    {
        int errorCode;
        char *errorMessage;
    #if DEBUG
        char *functionName;
        int lineNumber;
    #endif
    }
    

    使用它的最好方法是将方法的结果作为返回码返回(例如“FALSE 表示失败”或“文件指针或 NULL 如果失败”或“缓冲区大小或 0 如果失败"等)并传入一个ErrorInfo作为参数,如果出现故障,被调用的函数将填写该参数。

    这提供了丰富的错误报告:如果方法失败,您可以填写多个简单的错误代码(例如错误消息、代码行和失败文件,或其他)。它是一个结构的好处是,如果你以后想到什么有用的东西,你可以添加它 - 例如,在我上面的结构中,我允许调试构建包含错误的位置(文件/行),但您可以随时在其中添加整个调用堆栈的转储,而无需更改任何客户端代码。

    您可以使用全局函数来填写 ErrorInfo 以便可以干净地管理错误返回,并且您可以更新结构以轻松提供更多信息:

    if (error)
    {
        Error(pErrorInfo, 123, "It failed");
        return(FALSE);
    }
    

    ...您可以使用返回 FALSE、0 或 NULL 的此函数的变体,以允许将大多数错误返回表述为单行:

    if (error)
        return(ErrorNull(pErrorInfo, 123, "It failed"));
    

    这为您提供了其他语言中 Exception 类的许多优点(尽管调用者仍需要处理错误 - 调用者必须检查错误代码并且可能必须提前返回,但他们无能为力或下一步-to-nothing 并允许错误向上传播一系列调用方法,直到其中一个希望处理它,就像异常一样。

    此外,您还可以更进一步,创建一系列错误报告(如“InnerException”):

    struct ErrorInfo
    {
        int errorCode;
        char *errorMessage;
        ...
        ErrorInfo *pInnerError;    // Pointer to previous error that may have led to this one
    }
    

    然后,如果您从调用的函数中“捕获”错误,您可以创建一个新的更高级别的错误描述,并返回这些错误的链。例如“鼠标速度将恢复为默认值”(因为)“无法定位首选项块 'MousePrefs'”(因为)“XML 读取器失败”(因为)“找不到文件”。

    FILE *OpenFile(char *filename, ErrorInfo *pErrorInfo)
    {
        FILE *fp = fopen(filename, "rb");
        if (fp == NULL)
            return(ChainedErrorNull(pErrorInfo, "Couldn't open file"));
    
        return(fp);
    }
    
    XmlElement *ReadPreferenceXml(ErrorInfo *pErrorInfo)
    {
        if (OpenFile("prefs.xml", pErrorInfo) == NULL)
            return(ChainedErrorNull(pErrorInfo, "Couldn't read pref"));
        ...
    }
    
    char *ReadPreference(char *prefName, ErrorInfo *pErrorInfo)
    {
        XmlElement *pXml = ReadPreferenceXml(pErrorInfo);
        if (pXml == NULL)
            return(ChainedErrorNull(pErrorInfo, "Couldn't read pref"));
        ...
    }
    

    【讨论】:

      【解决方案9】:

      你可以变得非常愚蠢并做延续:

      void step_1(int a, int b, int c, void (*step_2)(int), void (*err)(void *) ) {
           if (!c) {
               err("c was 0");
           } else {
               int r = a + b/c;
               step_2(r);
           }
      }
      

      这实际上可能不是您想要做的,而是使用了多少函数式编程语言,更常见的是它们如何对代码进行建模以进行优化。

      【讨论】:

        【解决方案10】:

        我最近看到的就是这个惯用语:

        int err;
        do 
        {
          err = func1 (...);
          if (!err) break;
        
          err = func2 (...);
          if (!err) break;
        
          err = func3 (...);
          if (!err) break;
        
          /* add more calls here */
        
        } while (0);
        
        if (err)
        {
          /* handle the error here */
          return E_ERROR; /* or something else */
        }
         else 
        {
          return E_SUCCESS;
        }
        

        专业参数:

        它避免了 goto(为此滥用了 while(0) / break 组合)。你为什么想做这个?它降低了圈复杂度,并且仍然可以通过大多数静态代码分析器检查(MISRA 有人吗?)。对于需要针对圈复杂度进行测试的项目,这是上帝派来的,因为它将所有初始化的东西放在一起。

        反论点:

        do/while 循环结构的含义并不明显,因为循环结构被用作廉价的 goto 替换,而这只能在循环尾部看到。我第一次确信这个结构会引起很多“WTF”的时刻。

        至少需要一个注释来解释为什么要按要求的方式编写代码。

        【讨论】:

        • “循环的含义不明显”:可以使用宏(例如START_ERROR_HANDLING_BLOCK、CHECK_ERROR、END_ERROR_HANDLING_BLOCK,使用相当讨厌的冗长名称)来使代码的错误处理目的更加明显.
        • @Jason,我认为宏使意图更明显,但使实际流程和实现不那么明显。我通常讨厌隐藏在宏中的流控制代码。 @Nils,因为关于 goto 的意识形态废话而滥用 do/while 循环作为 goto 是可憎的......
        • @R..:我同意,必须小心使用宏。但是,如果您通常(在整个库中)使用这样的机制并且它被很好地命名和记录,那么宏可以帮助隐藏混乱的实现细节,同时实际澄清代码的“含义”。我个人倾向于为每个测试使用 SUCCESS() 风格的宏(这是一个常见的习惯用法,例如在 COM 代码中),而不是循环开始和循环结束的方法——它更安全,让您可以更好地控制每个特定情况.
        • @Jason,它仍然需要有人阅读代码才能熟悉您的特定库的习语,而简单的 if (failed) goto failure; 任何了解 C 的人都可以完全阅读。
        • @R..:确实。我并不是在争论使用宏,只是为了应用最佳实践如果和何时您使用它们。
        【解决方案11】:

        这是 IBM Unix 文章系列的一篇内容丰富的文章和测试文件:

        错误:UNIX 程序中的 errno

        使用标准错误机制

        https://www.ibm.com/developerworks/aix/library/au-errnovariable/

        另一个如何实现退出代码的好例子是 curl 的源代码(man 1 curl)。

        【讨论】:

        • errno 的一个恼人的地方是你不能自己使用它。所以,你不能利用现有的系统,你不仅要自己做,而且必须与他们的并行。您会在那篇文章中注意到 errno 是 (*__error()) 的宏,这意味着您不能分配它。也没有可以让您分配它的系统原语。所以,只是……烦人。
        • @Will,你大错特错了。 errno 是一个可写的左值,事实上某些标准库函数要求如果你想检测错误,你在调用它们之前显式地将 errno 归零(因为所有可能的返回值都在 co-函数域)。我认为您将 (*__error())(__error()) 混淆了,否则您不了解指针...
        【解决方案12】:

        如果您正在使用特定的上下文,我认为以下模式非常好。基本思想是对错误集状态的操作是空操作,因此可以将错误检查推迟到方便的时候!

        一个具体的例子:反序列化上下文。任何元素的解码都可能失败,但函数可能会继续运行而不会进行错误检查,因为当序列化记录处于错误状态时,所有decode_* 函数都是无操作的。插入decode_has_error 是方便、机会或优化的问题。在下面的示例中,没有错误检查,调用者将负责。

        void list_decode(struct serialization_record *rec,                       
                         struct list *list,                                     
                         void *(*child_decode)(struct serialization_record *)) {
            uint32_t length;                                                             
            decode_begin(rec, TAG);                                  
            decode_uint32(rec, &length);                                          
            for (uint32_t i = 0; i < length; i++) {                                
                list_append(list, child_decode(rec));
            }                                                                        
            decode_end(rec, TAG);
        }
        

        【讨论】:

          猜你喜欢
          • 2023-03-29
          • 2015-03-28
          • 2014-12-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多