Как использовать атрибут формата GCC printf с вариабельными шаблонами C++11?
У меня есть класс 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.
Я начал с пометки log()
функция с __attribute__ ((format (printf, 2, 3)))
(как this
это первый "скрытый" параметр, мне нужно сместить индексы параметров на единицу). Это не работает, потому что, если происходит сбой с ошибкой компиляции:
error: args to be formatted is not ‘...’
Затем я попытался добавить тот же атрибут к true_log()
функция. Он компилируется, но проверка ошибок на самом деле не выполняется: я пытался перейти к log()
некоторые недопустимые комбинации формат / переменная, и не было выдано предупреждение. Может быть, этот вид проверки "слишком поздний", или, другими словами, информация о переменной была потеряна в цепочке вызовов?
В крайнем случае, если я аннотировал log()
с __attribute__ ((format (printf, 2, 0)))
, Я получаю предупреждения о неправильных строках формата, но не будет выдана диагностика для недопустимых комбинаций формат / переменная.
Резюмируя проблему: как я могу иметь полную проверку формата из GCC, если я использую вариационные шаблоны C++11?
2 ответа
Я не верю, что ты можешь. Могу поспорить, что GCC проверяет строку формата, только если она является литералом. Вот почему выкладывать format
атрибут на true_log
не работает - эта функция вызывается с тем, что выглядит (синтаксически) как строка, определенная во время выполнения. Положить его на log
напрямую обойдется, но потребует format
атрибуты для поддержки шаблона с переменными значениями, что вы доказали, что это не так.
Я предлагаю вам взглянуть на больше C++- иш способов сделать форматированный вывод. Есть, например, boost::format
который работает так же, как printf, но динамически проверяет, что число и типы типов параметров соответствуют строке формата. Тем не менее, он не использует шаблоны с переменными значениями, а использует параметры, передаваемые ему (через оператор%), один за другим.
Для справки, я в итоге полностью удалил шаблоны 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
}
Если вы хотите использовать макрос, существует обходной путь.
Существуют конструкции, которые заставят компилятор выполнить проверку за вас, но не будут генерировать вызываемый код. Одна такая конструкция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
макрос будет мешать работе любой другой функции или метода с таким же именем.