实现自定义字符串格式化函数的可变参数检查

Ben*_*itz 4 c format printf compiler-warnings

Visual Studio 2015 引入了两个新警告 C4473 和 C4477,它们会在字符串格式化函数的格式字符串与关联的变量参数之间不匹配时发出通知:

warning C4473: 'printf' : not enough arguments passed for format string
warning C4477: 'printf' : format string '%p' requires an argument of type 'void *', but variadic argument 1 has type 'int'
Run Code Online (Sandbox Code Playgroud)

这些警告非常有帮助,并且已经被其他流行的编译器支持了一段时间(gcc 和 clang,-wformat我相信有选项,尽管我对这些编译器不太熟悉)。

现在我的问题是我想使用自定义Log(format, ...)函数来处理日志记录,这将执行额外的工作(例如,写入文件和控制台,或添加时间戳)。

但为了这个问题,我们假设我只是简单地调用printf

void Log(const char * format, ...)
{
    va_list args;
    va_start(args, format);
    printf(format, args);
    va_end(args);
}
Run Code Online (Sandbox Code Playgroud)

Log通过这样做,如果我使用不匹配的参数调用函数,则不会显示上面的警告:

printf("Error: %p\n", 'a'); // warning C4477
printf("Error: %p\n");      // warning C4473
Log("Error: %p\n", 'a');    // no warning
Log("Error: %p\n");         // no warning
Run Code Online (Sandbox Code Playgroud)

有没有办法告诉编译器它应该像检查函数一样检查函数的可变参数printf?特别是对于 MSVC 编译器,但适用于 gcc 和 clang 的解决方案也将受到赞赏。

Jon*_*ler 5

我不知道 VS 2015 或 VS 2017 中有什么可用(在Microsoft 文档中半休闲搜索没有提供任何说明)。然而,GCC 和 Clang 都支持声明性函数属性

\n\n
__attribute__((format(printf(,n,m)))\n
Run Code Online (Sandbox Code Playgroud)\n\n

可以将其分解为相当可移植的代码,如下所示:

\n\n
#if !defined(PRINTFLIKE)\n#if defined(__GNUC__)\n#define PRINTFLIKE(n,m) __attribute__((format(printf,n,m)))\n#else\n#define PRINTFLIKE(n,m) /* If only */\n#endif /* __GNUC__ */\n#endif /* PRINTFLIKE */\n\n\xe2\x80\xa6\n\nextern NORETURN void err_abort(const char *format, ...) PRINTFLIKE(1,2);\nextern NORETURN void err_error(const char *format, ...) PRINTFLIKE(1,2);\n\n\xe2\x80\xa6\n\nextern void err_logmsg(FILE *fp, int flags, int estat, const char *format, ...) PRINTFLIKE(4,5);\n\xe2\x80\xa6\nextern void err_remark(const char *format, ...) PRINTFLIKE(1,2);\n
Run Code Online (Sandbox Code Playgroud)\n\n

PRINTFLIKE(n,m)宏表示printf()格式字符串是 argument n,实际参数从 开始m。其中大多数都类似于printf()将格式字符串作为第一个参数和后面的数据。该err_logmsg()函数在参数 4 处的格式字符串之前有更多控制选项,但格式参数从 5 开始,紧随其后,有点像fprintf()将其格式字符串作为参数 2 而参数从参数 3 开始。

\n\n

设计一个参数位于格式字符串和变量参数列表之间的函数是可行的,例如:

\n\n
extern NORETURN void err_pos_error(const char *format, const char *filename, int lineno, const char *function, ...) PRINTFLIKE(1,5);\n
Run Code Online (Sandbox Code Playgroud)\n\n

可以这样调用:

\n\n
err_pos_error("Failed to open file \'%s\': %d - %s\\n", __FILE__, __LINE__, __func__, filename, errno, strerror(errno));\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们可以争论这是否是一个好的设计(出于各种原因,将 、 和 参数放在格式字符串之前而不是之后可能会更好),但这是一个可行的设计,可以演示__FILE____LINE__连续数字__func__PRINTFLIKE宏 \xe2\x80\x94 中或使用__attribute__((format(printf,n,m))).

\n\n

这些NORETURN东西是对识别不返回的函数的宏支持:

\n\n
#if !defined(NORETURN)\n#if __STDC_VERSION__ >= 201112L\n#define NORETURN      _Noreturn\n#elif defined(__GNUC__)\n#define NORETURN      __attribute__((noreturn))\n#else\n#define NORETURN      /* If only */\n#endif /* __STDC_VERSION__ || __GNUC__ */\n#endif /* NORETURN */\n
Run Code Online (Sandbox Code Playgroud)\n\n

我所基于的代码可以在GitHub 上的SOQ(Stack Overflow Questions)存储库中以文件形式stderr.csrc/libsoq子目录stderr.h中找到。

\n