自定义错误处理器导致的 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 会保守地认为以下路径可能存在:
- 进入
default; - 调用
handle_error(); handle_error()返回;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 语句而修改错误处理函数的返回类型,同样无法准确表达该函数不会返回。
备注:内容仅供参考。
