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

时间:2019-01-04 02:56:02

标签: 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'

这些警告非常有用,并且其他流行的编译器(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的解决方案。

2 个答案:

答案 0 :(得分:2)

我不知道VS 2015或VS 2017提供了哪些功能(Microsoft documentation上的半临时搜索未提供任何说明)。但是,GCC和Clang都支持声明性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这个东西是宏支持,用于标识不返回的函数:

#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 */

基于此的代码可在GitHub上的SOQ(堆栈溢出问题)存储库中以src/libsoq子文件夹中的文件stderr.cstderr.h的形式获得目录。

答案 1 :(得分:0)

所以看来我确实对Visual Studio不走运。

正如乔纳森(Jonathan)在回答中提到的那样,可以对GCC和Clang做到这一点。 this answer中也对此进行了解释。

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

还有一个替代方法,我决定不使用(我将解释原因)。 Microsoft提供了他们所谓的SAL annotation(用于源代码注释语言)。可以用_Printf_format_string_之类的注释函数,以获得我所要的内容。例如,this answer中对此进行了描述。

缺点是,默认情况下编译器会完全忽略它。只有在使用/analysis参数或从项目的属性窗口中启用代码分析后,这些注释才真正被评估。此分析进行了大量检查;默认情况下,它使用Microsoft Native Recommended Rules,但是可以自定义要检查的内容,甚至可以仅检查字符串格式。

但是即使到那时,对于像我这样的相对较小的项目,编译时间的开销也不值得付出痛苦。