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

Posted

技术标签:

【中文标题】为自定义字符串格式化函数实现可变参数检查【英文标题】:Implement variadic arguments checking for custom string formatting functions 【发布时间】:2019-01-04 02:56:02 【问题描述】:

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 的解决方案将不胜感激。

【问题讨论】:

对于 gcc 和可能的 clang,有 format attribute。 Visual C++ 有相应的__declspec 关键字,但它似乎没有格式字符串的说明符。你可以找到例如的声明printf 在头文件中,看看是否有一些特殊的声明关键字或标识符。如果没有,那你就不走运了。 也许在调试模式下调用Log() 也调用snprintf(0,0 ...;? (虽然对于带有副作用的 args 有点草率。) 【参考方案1】:

我不知道 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(堆栈溢出问题)存储库中以文件 stderr.cstderr.h 的形式在 src/libsoq 子目录中找到。

【讨论】:

请注意,对于成员函数,您必须为隐藏的第一个参数添加一个参数。它在文档中这么说。 什么是“成员函数”,@F***?这是 C! 我只是想指出,尽管它是 C 特性,但它也适用于 C++ 类的成员函数。【参考方案2】:

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

正如乔纳森在他的回答中提到的,GCC 和 Clang 都可以做到这一点。这在this answer 中也有解释。

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

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

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

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

【讨论】:

使用clang-cl总是可以的:clang.llvm.org/docs/MSVCCompatibility.html

以上是关于为自定义字符串格式化函数实现可变参数检查的主要内容,如果未能解决你的问题,请参考以下文章

如何在spring data jpa中为自定义采石场提供可变参数而不使用for循环

_vsnprintf在可变参数打印中的用法

如何检查NSString格式是否包含与可变参数相同数量的说明符?

C语言中如何实现可变参函数

C语言奇淫技巧之函数的可变参数

可变参数省略号使用简介