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

Implement variadic arguments checking for custom string formatting functions

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'

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

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

但是为了这个问题,我们假设我只是简单地结束对 printf:

的调用
void Log(const char * format, ...)
{
    va_list args;
    va_start(args, format);
    printf(format, args);
    va_end(args);
}

通过这样做,如果我使用不匹配的参数调用 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

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

我不知道 VS 2015 或 VS 2017 中有什么可用(半随意搜索 Microsoft documentation didn't provide any illumination). However, GCC and Clang both support a declarative function attribute:

__attribute__((format(printf(,n,m)))

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

#if !defined(PRINTFLIKE)
#if defined(__GNUC__)
#define PRINTFLIKE(n,m) __attribute__((format(printf,n,m)))
#else
#define PRINTFLIKE(n,m) /* If only */
#endif /* __GNUC__ */
#endif /* PRINTFLIKE */

…

extern NORETURN void err_abort(const char *format, ...) PRINTFLIKE(1,2);
extern NORETURN void err_error(const char *format, ...) PRINTFLIKE(1,2);

…

extern void err_logmsg(FILE *fp, int flags, int estat, const char *format, ...) PRINTFLIKE(4,5);
…
extern void err_remark(const char *format, ...) PRINTFLIKE(1,2);

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

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

extern NORETURN void err_pos_error(const char *format, const char *filename, int lineno, const char *function, ...) PRINTFLIKE(1,5);

可以这样调用:

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

我们可以讨论这是否是一个好的设计(最好将 __FILE____LINE____func__ 参数放在格式字符串之前,而不是之后,因为各种原因),但它是一个可行的设计,它在 PRINTFLIKE 宏中演示了非连续数字 — 或者使用 __attribute__((format(printf,n,m))).

NORETURN 东西是宏支持,用于识别不 return:

的函数
#if !defined(NORETURN)
#if __STDC_VERSION__ >= 201112L
#define NORETURN      _Noreturn
#elif defined(__GNUC__)
#define NORETURN      __attribute__((noreturn))
#else
#define NORETURN      /* If only */
#endif /* __STDC_VERSION__ || __GNUC__ */
#endif /* NORETURN */

我所基于的代码可以在我的 SOQ (Stack Overflow Questions) repository on GitHub as files stderr.c and stderr.h in the src/libsoq 子目录中找到。

看来我确实不走运Visual Studio。

正如乔纳森在他的回答中提到的那样,可以同时使用 GCC 和 Clang 来做到这一点。这在 this answer.

中也有解释

然而,虽然 Visual Studio 似乎为 printf 和许多其他标准函数输出警告,但这或多或少在编译器中是硬编码的,并且不能扩展到自定义函数。

还有一个替代方案,我决定不使用(我会解释原因)。例如,Microsoft 提供了他们所谓的 SAL annotation (for source code annotation language). It is possible to annotate a function with things like _Printf_format_string_ in order to get what I was asking. This is described in this answer

缺点是,编译器默认完全忽略它。仅当您使用 /analysis 参数或从项目的 属性 window 启用代码分析时,才会实际评估这些注释。这个分析做了很多检查;默认情况下它使用 Microsoft Native Recommended Rules,但可以自定义要检查的内容,甚至可以只检查字符串格式。

但即便如此,对于像我这样相对较小的项目来说,编译时间的开销还是不值得的。