请考虑以下示例:

a.c

#include <stdlib.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>

char *invocation_name;

static void handle_error(const char* fmt, ...) {
    va_list args;
    va_start(args, fmt);
    fprintf(stderr, "%s: ", invocation_name);
    vfprintf(stderr, fmt, args);
    puts("");
    va_end(args);
    exit(1);
}

static int open_a_file(void) {
    int fd = open("non-existent", 0);
    switch (fd) {
        default: return 0;
        case -1: handle_error("something happened"); 
    }
}

int main(int argc, char **argv)
{
    (void) argc; /* unused */
    invocation_name = argv[0];
    open_a_file();
}

这个例子有点牵强,但这样就比较小了。在实际程序中,我有一个函数,它接受并返回一个字符串表示,或者如果超出范围int则退出并出现错误。int

当我编译它时:

$ gcc -Wall -Wextra -pedantic -Wmissing-prototypes -Wstrict-prototypes \
    -Wold-style-definition -std=c99 -O3 \
    a.c
a.c: In function 'open_a_file':
a.c:24:1: warning: control reaches end of non-void function [-Wreturn-type]
   24 | }
      | ^

我怎样才能让自己gcc快乐?

UPD我想我必须提供遇到问题时已有的功能,而不是为问题发明新功能。但我希望问题小且易于重现… 无论如何,想象一下以下函数代替open_a_file()

static const char *attr_name(uint16_t type) {
    switch (type) {
        case ATTR_MAPPED_ADDRESS:      return "MAPPED-ADDRESS";
        case ATTR_XOR_MAPPED_ADDRESS:  return "XOR-MAPPED-ADDRESS";
        case ATTR_SOFTWARE:            return "SOFTWARE";
        case ATTR_FINGERPRINT:         return "FINGERPRINT";
        case ATTR_RESPONSE_ORIGIN:     return "RESPONSE-ORIGIN";
        default: handle_error("attr_name: unknown attr type (0xx)", type);
    }
}

我 不能 这么做return handle_error(..., 因为 那样 我就 必须handle_error()退货int.

该程序处于早期阶段(实际上可能仍是一个原型),所以是的,错误处理可能有些原始或基本。并且可能会发生attr_name()接收未知类型的情况。如果程序收到一个具有未知类型的数据包。

我确实计划让解析 UDP 数据包时返回错误,如果我不放弃开发,我会利用一些库,这样警告可能在不久的将来自行解决。但目前这就是我所得到的。

3

  • 1
    更改switchif(fd == -1) handle_error("something happened"); return fd;


    – 

  • 不应该open_a_file()返回fd吗?如果它仅返回0或退出,则返回值毫无意义。更不用说泄漏打开的文件了。


    – 


  • 1
    也许我没有理解这个问题的要点,但对我来说答案很简单:添加 return。有什么理由不这样做吗?


    – 


最佳答案
3

noreturn使用在中定义的便捷宏,<stdnoreturn.h>它扩展为

#include <stdnoreturn.h>
noreturn static void handle_error(const char* fmt, ...) {
    ...
}

C23(-std=c2x)引入了属性,因此如果您不需要兼容性,您可以使用它:

[[noreturn]] static void handle_error(const char* fmt, ...) {
    ...
}

5

  • 那不是一个非void功能。错误来自int open_a_file()


    – 


  • 2
    @WeatherVane 但是如果编译器看到它要么返回 0 要么调用一个_Noreturn函数,它就知道它永远不会掉到最后。


    – 

  • @Barmar 正在解决错误的问题。


    – 

  • 2
    @WeatherVane:它正在解决正确的问题。问题是编译器对的分析使其open_a_file具有流向}具有非 void 返回类型的函数结束的可能路径。声明handle_errorwith_Noreturn会更改该分析,因此编译器会发现没有路径流向open_a_file}然后不会发出诊断消息,这是正确且理想的。


    – 


  • 请注意,_Noreturn至少需要 C11(并且在 C23 中已弃用)。


    – 

编译器不知道如果会发生什么fd==-1

只需改变处理情况的方式:

static int open_a_file(void) 
{
    int fd = open("non-existent", 0);
    if(fd == -1) handle_error("something happened"); 
    return 0;
}

或者在最后添加返回语句(不会增加代码大小)

static int open_a_file(void) 
{
    int fd = open("non-existent", 0);
    switch (fd) 
    {
        default: return 0;
        case -1: handle_error("something happened");  
    }
    return 0;
}

7

  • 留下悬而未决的文件。


    – 

  • @WeatherVane:为什么?handle_error当且仅当打开文件时发生错误时,代码才会调用。如果发生错误,则不会打开文件。如果没有发生错误,例程将返回并打开文件,这是预期的。(OP 的示例程序没有显示关闭,但那是因为它只是问题的演示代码,而不是完整的程序。完整的程序在处理完文件后会关闭。)


    – 


  • @EricPostpischil 因为它无法fd供调用者使用。


    – 

  • 1
    @WeatherVane:这是 OP 的演示代码中的一个错误,而不是这个答案。


    – 

  • @EricPostpischil 这个答案中存在错误。


    – 

这全都与正确的程序设计有关。

正确的程序是永远不要让某些中间层代码进行错误决策并使用 exit() 等关闭程序。相反,我们应该设计程序,以便所有错误都返回给调用者,并且错误处理程序位于应用程序的顶层,这是唯一可以做出关闭决定的一方。

一个明显的原因是,除了特定模块所知道的之外,可能还有其他需要执行的清理代码。但将所有错误处理和决策集中到一个地方也对维护有很大帮助。例如,有些错误可能并不严重到需要关闭。

因此,所有专业的库设计都倾向于将每个 API 函数的返回值保留为结果/错误代码。在实际程序中,仅将 true/false 或 0/-1 作为结果太过直白 – 需要更多信息,因此通常使用enum。任何需要返回的值都通过参数传递。

fctrl.h将 1970 年代的旧臭鼬软件隐藏在包装函数库中确实有意义-stdio.h如果那个库写得不是那么糟糕,本质上应该做什么fctrl.h……人们必须意识到大多数旧的 Unix 垃圾不是一些可以用来寻找灵感的规范模型,而是令人沮丧的例子,教你永远不要编写代码,这与 C 代码的正确编程实践相去甚远。

因此,我们将代码重写如下:

typedef enum
{
  FILE_OK,         // number 0 traditionally used for "no error"
  FILE_ERR_OPEN,
  /* add as many error codes as needed */
} file_err_t;

file_err_t open_a_file (int* fd) {  // we do need to return the file descriptior somewhere
    *fd = open("non-existent", 0);

    // in a real application we ought to check errno here and translate to an error code
    // but for now...:
    if(*fd == -1)
      return FILE_ERR_OPEN; 
    return FILE_OK;
}

请注意,我们的包装器库确实应该处理所有文件描述符内容以及 1970 年代的errno错误处理。errno如果需要,可以使用相同数量的细节或一些简化的抽象来转换为我们的自定义枚举。

下一步是将有关错误严重程度的知识从中间层移到错误处理程序中。在这种情况下,我们可能还会将有关要显示什么消息的知识放入错误处理程序中。

而当我们这样做时,我们可能会注意到,另一个 1970 年代的臭鼬软件库stdarg.h是一个祸害 – 它不应该出现在任何程序中,因为它不存在安全性,我们真的不需要错误处理程序(或任何其他函数)来拥有可变数量的参数。这种需要来自混乱的程序设计。很有可能我们的错误处理程序本身就会导致很多错误,这显然不是什么好事。

理想情况下,我们应该将错误处理程序代码放在一个单独的文件中。为属于该错误处理程序的所有函数添加适当的代码前缀。file_err_t如果这是整个项目的集中错误类型,那么可能实际上应该放在错误处理程序的标题中。

static const char* invocation;

void err_handler_init (const char* invocation_name)
{
  invocation = invocation_name;
}

void err_handler (file_err_t err) 
{
    switch(err)
    {
       /* all decision making is done here, not in at some random place in the program */
       case FILE_ERR_OPEN: 
         fprintf(stderr, "%s: Couldn't open file.", invocation_name);
         exit(1); // this particular error merits a close down
         break;
    }
}

然后 main.c 变成这样:

#include "file_handling.h"
#include "error_handler.h"

int main(int argc, char **argv)
{
    (void) argc; /* unused */
    err_handler_init(argv[0]);
    
    int fd;
    file_err_t result = open_a_file(&fd);
    if(result != FILE_OK)
    {
        err_handler(result);
    }
    /* do something with the file */
}

3

  • 我承认设计有些欠缺。或者说这个“项目”还处于早期阶段,人们可能称之为 PoC,我基本上是在尝试发出请求并处理(并输出)响应。在下一步中,我计划不仅输出响应,而且至少先返回它,然后问题可能会消失。另一方面,它不应该是什么大问题。我想你可能会称它为健康检查器。它接受 HTTP 请求,发出 STUN 请求,如果一切顺利,则做出响应……


    – 

  • …它可能会死掉,只要它重新启动(例如docker run --restart always)。我甚至写了一个,它只处理一个请求并退出,然后被唤醒docker。所以一般来说我同意,但在某些情况下,退出任何地方至少都是可以忍受的。无论如何,谢谢你的想法。它可能把我推向了正确的方向。说到这个,我想说,这甚至enum可能还不够……


    – 

  • …我希望在顶部包含尽可能多的信息(它尝试打开哪个文件?确切的退出代码是什么?也许还有一些其他数据),这样它就不必报告,比如“一个未知函数执行了意外操作,我完全不知道我要做什么。”但我不喜欢引入全局变量的想法,这似乎是本例中唯一的选择。顺便说一句,我让问题中的例子更像“现实生活”。


    –