我有一个C ++类,它是日志记录系统的前端。它的日志记录功能是使用C ++ 11的可变参数模板实现的:
template <typename... Args>
void Frontend::log(const char *fmt, Args&&... args) {
backend->true_log(fmt, std::forward<Args>(args)...);
}
每个日志记录后端都实现自己的true_log
版本,除其他外,它使用转发的参数来调用vsnprintf
。 E.g:
void Backend::true_log(const char *fmt, ...) {
// other stuff..
va_list ap;
va_start(ap, fmt);
vsnprintf(buffer, buffer_length, fmt, ap);
va_end(ap);
// other stuff..
}
一切都很好,我很高兴。
现在,我想对log()
参数添加静态检查:具体来说,我想使用GCC的printf格式属性。
我首先使用log()
标记__attribute__ ((format (printf, 2, 3)))
函数(因为this
是第一个“隐藏”参数,我需要将参数索引换一)。这不起作用,因为如果失败并出现编译错误:
error: args to be formatted is not ‘...’
然后,我尝试将相同的属性添加到true_log()
函数中。它编译,但实际上没有执行错误检查:我试图传递给log()
一些无效的格式/变量组合,并且没有发出警告。也许这种检查“太晚了”,换句话说,有关变量的信息已经在调用链中丢失了?
作为最后的手段,如果我用log()
注释__attribute__ ((format (printf, 2, 0)))
,我会收到有关错误格式字符串的警告,但不会对无效的格式/变量组合发出诊断。
总结问题:如果我使用C ++ 11的可变参数模板,如何从GCC进行全面格式检查?
答案 0 :(得分:2)
我不相信你可以。我敢打赌,GCC只会验证格式字符串,如果它是 literal 。这就是为什么在format
上放置true_log
属性不起作用的原因 - 该函数的调用(语法上)类似于运行时确定的字符串。直接将它放在log
上会绕过这一点,但是需要format
属性来支持可变参数模板,而事实证明它不是。
我建议你看看更多C ++ - 是做格式化输出的方法。例如,boost::format
有点像printf,但动态验证参数类型的数量和类型是否与格式字符串匹配。但是它不使用可变参数模板,而是逐个消耗输入它的参数(通过operator%)。
答案 1 :(得分:1)
为了记录,我最终完全删除了C ++ 11可变参数模板,并使用传统的va_list
。
__attribute__((format(printf, 2, 3)))
void Frontend::log(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
backend->true_log(fmt, ap);
va_end(ap);
}
void Backend::true_log(const char *fmt, va_list ap) {
// log the message somehow
}
答案 2 :(得分:0)
如果您愿意使用宏,则有一种解决方法。
有些结构会导致编译器为您进行检查,但不会生成任何被调用的代码。 sizeof
是一种这样的构造。因此,您可以为记录器使用宏,以在printf
计算的上下文中直接将参数传递给sizeof
,然后调用记录器本身。
使用宏的原因是确保像对待字符串文字一样对待格式字符串。
在下面的插图中,我将sizeof
计算视为一个一次性参数,但是应该有其他方法可以应用相同的技术。
template <typename... Ts>
void Frontend::log(size_t, const char *fmt, Ts&&... args) {
backend->true_log(fmt, std::forward<Ts>(args)...);
}
#define log(...) log(sizeof(printf(__VA_ARGS__)), __VA_ARGS__)
当然,这是一种解决方法。有很多原因不使用宏。并且在这种情况下,log
宏将干扰具有相同名称的任何其他函数或方法。