【问题标题】:How to use GCC's printf format attribute with C++11 variadic templates?如何将 GCC 的 printf 格式属性与 C++11 可变参数模板一起使用?
【发布时间】:2012-09-16 10:34:05
【问题描述】:

我有一个 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。例如:

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 格式属性。

我首先用__attribute__ ((format (printf, 2, 3))) 标记log() 函数(因为this 是第一个“隐藏”参数,我需要将参数索引移动一个)。这不起作用,因为如果失败并出现编译错误:

error: args to be formatted is not ‘...’

然后,我尝试将相同的属性添加到true_log() 函数。它编译,但实际上没有执行错误检查:我试图传递给log()一些无效的格式/变量组合,并且没有发出警告。也许这种检查“为时已晚”,或者换句话说,关于变量的信息已经在调用链中丢失了?

作为最后的手段,如果我用__attribute__ ((format (printf, 2, 0))) 注释log(),我会收到有关错误格式字符串的警告,但不会针对无效的格式/变量组合发出诊断。

问题总结:如果我使用 C++11 的可变参数模板,如何从 GCC 进行完整的格式检查?

【问题讨论】:

  • 既然 vsnprintf() 不能处理比​​老派 ... 所能做的更多的事情,为什么要首先使用可变参数模板呢?
  • 当您只是丢弃类型信息时,为什么还要使用可变参数模板?只需将 true_log() 设为您真正的日志记录功能。
  • 或者让Frontend::log接受一个可变参数...

标签: c++ gcc c++11 variadic-templates function-attributes


【解决方案1】:

我不相信你可以。我敢打赌,如果格式字符串是 literal,GCC 只会验证格式字符串。这就是为什么将 format 属性放在 true_log 上不起作用的原因 - 该函数被调用时看起来(在语法上)类似于运行时确定的字符串。将它直接放在log 上会规避这一点,但需要format 属性来支持可变参数模板,而您证明它不支持。

我建议你看看更多的 C++-ish 方法来进行格式化输出。例如,boost::format 的工作方式类似于 printf,但会动态验证参数类型的数量和类型是否与格式字符串匹配。不过,它不使用可变参数模板,而是一个接一个地使用(通过运算符 %)提供给它的参数。

【讨论】:

    【解决方案2】:

    为了记录,我最终完全删除了 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
    }
    

    【讨论】:

      【解决方案3】:

      如果您愿意使用宏,有一种解决方法。

      有些结构会导致编译器为您进行检查,但不会生成任何调用代码。一种这样的构造是sizeof。因此,您可以使用 logger 的宏将参数直接传递给 printf,但在 sizeof 计算的上下文中,然后调用 logger 本身。

      使用宏的原因是确保格式字符串的处理方式与处理字符串文字的方式相同。

      在下图中,我将 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__)
      

      Try it online!

      当然,这是一种解决方法。不使用宏的原因有很多。在这种情况下,log 宏会干扰任何其他同名的函数或方法。

      【讨论】:

      • 请注意转发__VA_ARGS__ 你应该使用##__VA_ARGS__除此之外我真的很喜欢这个解决方案。
      • 另外你应该把宏改成这样:#define log(f, ...) true_log(sizeof(printf(f,##__VA_ARGS__)), f, ##__VA_ARGS)
      • @SebastianCabot:所以,由于格式字符串是必需的参数,我不需要使用非标准的逗号吞咽运算符。
      • 好点!非常感谢您指出他们
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-08-30
      • 1970-01-01
      • 2021-08-27
      • 2023-04-07
      • 2012-11-15
      • 1970-01-01
      相关资源
      最近更新 更多