PAYATHON 2026

自定义错误处理器导致的 control reaches end of non-void function 警告

排障赵师傅

结论

应将 handle_error() 声明为“不返回的函数”。当前代码使用 -std=c99,可以添加 GCC 的 noreturn 属性:

static void handle_error(const char *fmt, ...)
    __attribute__((noreturn, format(printf, 1, 2)));

GCC 由此可以确定,调用 handle_error() 后的控制流不可达,也就不会再报告 control reaches end of non-void function。

为什么会出现警告

从函数类型来看,handle_error() 是普通的 void 函数,而普通 void 函数执行完后可以返回调用者。

尽管函数体调用了 exit(1),编译器也不一定会根据函数实现推断出它永远不会返回。分析 attr_name() 时,GCC 会保守地认为以下路径可能存在:

  1. 进入 default;
  2. 调用 handle_error();
  3. handle_error() 返回;
  4. attr_name() 执行到函数末尾,但没有返回 const char *。

返回类型不是 void 的函数不能沿这条路径结束,因此触发了 -Wreturn-type。

适用于当前 C99 编译方式的写法

先声明函数并添加 GCC 属性,然后再提供定义:

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

static void handle_error(const char *fmt, ...)
    __attribute__((noreturn, format(printf, 1, 2)));

static void handle_error(const char *fmt, ...)
{
    va_list args;

    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    fputc('\n', stderr);
    va_end(args);

    exit(EXIT_FAILURE);
}

这里的 noreturn 表示函数不会返回调用者。format(printf, 1, 2) 表示第一个参数是格式字符串,格式化参数从第二个参数开始,GCC 可以据此检查格式字符串和参数类型。这个属性与当前警告无关,但很适合用于此类函数。

attr_name() 可以继续使用原来的控制流结构:

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 (0x%02x)",
            (unsigned int)type
        );
    }
}

调用 handle_error() 时,带有 noreturn 属性的声明必须已经可见。如果声明放在头文件中,该属性也应写在头文件的声明上。

代码还将 type 显式转换成了 unsigned int,因为 %x 对应的参数类型是 unsigned int。直接传入 uint16_t 会发生整数提升,提升后的具体类型取决于平台上的类型范围。显式转换可以避免可变参数与格式说明符的类型不匹配。

使用 C11 标准写法

如果可以将编译标准改为 C11,可以使用标准关键字 _Noreturn:

static _Noreturn void handle_error(const char *fmt, ...)
{
    va_list args;

    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    fputc('\n', stderr);
    va_end(args);

    exit(EXIT_FAILURE);
}

编译选项至少应改为:

gcc -std=c11 -Wall -Wextra -pedantic a.c

C11 还提供了 <stdnoreturn.h> 中的 noreturn 宏:

#include <stdnoreturn.h>

static noreturn void handle_error(const char *fmt, ...);

如果仍然使用 -std=c99,则不能使用 _Noreturn 或 <stdnoreturn.h>,因为它们不属于 C99。此时应继续使用 GCC 的 __attribute__((noreturn))。

兼容 GCC 和其他编译器

如果代码既要支持 GCC 系编译器,也要兼容不识别这些属性的编译器,可以封装成宏:

#if defined(__GNUC__) || defined(__clang__)
#define NORETURN __attribute__((noreturn))
#define PRINTF_LIKE(fmt_index, first_arg) \
    __attribute__((format(printf, fmt_index, first_arg)))
#else
#define NORETURN
#define PRINTF_LIKE(fmt_index, first_arg)
#endif

static void handle_error(const char *fmt, ...)
    NORETURN PRINTF_LIKE(1, 2);

支持 C11 的项目可以优先使用 _Noreturn,针对旧标准再使用编译器扩展作为兼容方案。

不建议简单添加虚假返回值

在 handle_error() 后添加返回语句也能消除警告:

default:
    handle_error("unknown type");
    return NULL;

这种写法没有准确表达函数的行为。handle_error() 的设计就是终止程序,应当在声明中明确注明它不会返回。如果以后误删了 exit(),编译器也更容易发现声明与实现不一致。

如果想为后续修改增加运行时保护,可以在调用后添加 abort():

default:
    handle_error("unknown type");
    abort();

这样即使修改后的 handle_error() 意外返回,程序也不会继续执行。正确标注 handle_error() 后,通常不需要再这样处理。

也不能写成:

return handle_error(...);

handle_error() 的返回类型是 void,而 attr_name() 必须返回 const char *。为了配合这条 return 语句而修改错误处理函数的返回类型,同样无法准确表达该函数不会返回。

备注:内容仅供参考。