Различные статьи по Qt и c++

Обзор spdlog и fmt: зачем нужно профессиональное логирование и форматирование строк

Введение

Qt предоставляет qDebug()/qWarning()/qCritical() и QString::arg() для логирования и форматирования строк — для небольших приложений этого достаточно. Но по мере роста проекта (особенно серверной части, не привязанной к Qt-событиям, или высоконагруженного кода, где логирование не должно заметно влиять на производительность) встроенных средств начинает не хватать: нет гибкой настройки уровней логирования по модулям, нет встроенной ротации файлов логов, нет асинхронной записи. spdlog и fmt — связанные между собой C++ библиотеки (spdlog использует fmt внутри для форматирования), решающие именно эти задачи. В статье разберём, что каждая из них даёт и когда оправдан переход на них.

Концепция

fmt — библиотека форматирования строк, вдохновлённая Python str.format(), типобезопасная (в отличие от printf, ошибка типа аргумента — ошибка компиляции, а не неопределённое поведение во время выполнения) и значительно быстрее std::ostringstream. spdlog — библиотека логирования поверх fmt, предоставляющая именованные логгеры, уровни важности сообщений (trace/debug/info/warn/error/critical), несколько одновременных «приёмников» (sinks) вывода — консоль, файл, ротируемый файл — и асинхронный режим записи, не блокирующий основной поток приложения на время записи в файл.

Пример кода: Обзор spdlog и fmt

// Сравнение: форматирование строки тремя способами

// 1. C-style printf — типы не проверяются компилятором, легко передать неверный тип
char buffer[256];
snprintf(buffer, sizeof(buffer), "Устройство %s: температура %f", "esp32_01", 23.5); // OK
// snprintf(buffer, sizeof(buffer), "Устройство %s: температура %f", "esp32_01", "23.5"); // UB, не ошибка компиляции!

// 2. Qt QString::arg() — типобезопасно для Qt-типов, но многословно для длинных строк
QString qtMessage = QString("Устройство %1: температура %2").arg("esp32_01").arg(23.5);

// 3. fmt::format — типобезопасно, кратко, и быстрее std::ostringstream
#include <fmt/core.h>
std::string fmtMessage = fmt::format("Устройство {}: температура {}", "esp32_01", 23.5);
// fmt::format("Устройство {}: температура {}", "esp32_01", "23.5"); // тоже скомпилируется —
// fmt принимает аргументы любого форматируемого типа, в т.ч. строки на месте чисел,
// но именно ТИПОБЕЗОПАСНО — никакого UB, просто строка выведется как строка
// Сравнение логирования: qDebug() против spdlog
#include <QDebug>
#include <spdlog/spdlog.h>

void logComparisonExample()
{
    qDebug() << "Получено показание от устройства" << "esp32_01" << "температура" << 23.5;
    // Простой вывод в консоль/qInstallMessageHandler — без уровней важности с фильтрацией,
    // без ротации файлов "из коробки", без возможности направить разные модули в разные файлы

    spdlog::info("Получено показание от устройства {}: температура {}", "esp32_01", 23.5);
    spdlog::warn("Устройство {} не отвечает более {} секунд", "esp32_02", 30);
    spdlog::error("Не удалось подключиться к брокеру MQTT: {}", "connection refused");
    // Уровни важности (info/warn/error) позволяют фильтровать вывод без изменения кода —
    // например, отключить уровень debug в продакшене, оставив только warn и выше
}

Пояснения к коду

fmt::format() использует фигурные скобки {} как плейсхолдеры (синтаксис, похожий на Python), и в отличие от printf, не требует указания типа аргумента в самой строке формата (%d, %s, %f) — тип определяется автоматически на основе типа переданного аргумента во время компиляции, что устраняет целый класс ошибок несоответствия типов, до сих пор распространённых в C-style форматировании. spdlog::info()/warn()/error() принимают строку формата и аргументы в том же синтаксисе, что и fmt::format() (поскольку spdlog использует fmt внутри), но дополнительно ассоциируют сообщение с уровнем важности — это позволяет в дальнейшем (статья 335) настроить минимальный уровень вывода глобально, без необходимости вручную оборачивать каждый вызов в условие if (debugEnabled).

Подводные камни

  • Внедрение spdlog/fmt во весь проект сразу, без оценки реальной необходимости — если приложение небольшое, логирование простое, и qDebug() полностью устраивает по функциональности, переход на дополнительную зависимость добавляет сложность сборки (статья 334) без соразмерной выгоды; spdlog оправдан прежде всего для серверных компонентов, высокопроизводительного кода, или проектов с серьёзными требованиями к управлению логами в продакшене.
  • Смешивание qDebug() и spdlog в одном модуле без согласованной стратегии, что приводит к разрозненному, нескоординированному выводу логов из разных частей одного приложения — если решено использовать spdlog, стоит либо полностью заменить использование qDebug() в соответствующих модулях, либо явно интегрировать оба механизма так, чтобы они писали в общий, согласованный поток вывода (статья 338 разбирает именно эту интеграцию).
  • Линковка fmt как header-only библиотеки в больших проектах с большим количеством единиц компиляции без понимания, что это увеличивает время компиляции каждого файла, использующего fmt::format — для крупных проектов часто предпочтительнее собирать fmt как обычную, скомпилированную статическую библиотеку, а не header-only вариант, что разбирается в статье 334.
  • Использование fmt исключительно ради синтаксиса {} без реального использования его более продвинутых возможностей (форматирование чисел с заданной точностью, выравнивание, локализация) — для простой конкатенации строк без особых требований к форматированию выгода от перехода с QString::arg() на fmt::format() минимальна, и реальная польза проявляется в более сложных случаях форматирования, разбираемых в статье 337.