Как использовать атрибут формата печати GCC с С++ 11 вариационными шаблонами?

У меня есть класс С++, который является интерфейсом для системы ведения журнала. Его функция ведения журнала реализована с использованием вариативных шаблонов С++ 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, если я использую вариативные шаблоны С++ 11?

Ответ 1

Я не верю, что ты можешь. Бьюсь об заклад, что GCC проверяет только строку формата, если это буквальный. Вот почему установка атрибута format на true_log не работает - эта функция вызывается с тем, что выглядит (синтаксически) как строка, определенная во время выполнения. Включение его в log напрямую обойдется, но потребует атрибутов format для поддержки вариационного шаблона, который, как вы доказали, это не так.

Я предлагаю вам взглянуть на более С++-ish способ сделать форматированный вывод. Существует, например, boost::format, который работает как printf, но динамически проверяет, соответствует ли число и типы параметров строкам формата. Однако он не использует вариационные шаблоны, но вместо этого потребляет параметры, подаваемые на него (через оператора%) один за другим.

Ответ 2

Для записи я в конечном итоге удалил вариативные шаблоны С++ 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. Таким образом, вы можете использовать макрос для своего регистратора, чтобы передать аргументы непосредственно в 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 будет мешать любой другой функции или методу с таким же именем.