Бібліотеки C++: fmt, spdlog і nlohmann/json
Бібліотеки C++: fmt, spdlog і nlohmann/json
🎯 Мета лекції
- Форматувати текст, числа й контейнери через
fmt, включно з власними типами. - Налаштувати логування
spdlog: рівні, виведення в консоль і файл, ротація, шаблон повідомлення. - Створювати, читати, зберігати та перевіряти JSON за допомогою
nlohmann/json. - Підключати кожну з бібліотек через vcpkg та
find_package.
🔑 Ключові терміни
- Рядок формату (format string): шаблон із
{}, у який підставляються значення. - Рівень логування (log level): важливість повідомлення (
debug,info,warning,error). - Sink: місце призначення повідомлень (консоль, файл).
- Серіалізація: перетворення об'єкта на текст (JSON) і назад.
Далі розглядаються конкретні бібліотеки, які використовуються майже в кожному C++ проєкті: вивід тексту, журнал подій і обмін даними. Усі приклади зібрані та запущені в одному vcpkg-проєкті, а виводи в статті реальні.
Підготовка проєкту
Три бібліотеки описуються одним маніфестом:
{
"name": "lib-demo",
"version": "1.0.0",
"dependencies": ["fmt", "spdlog", "nlohmann-json"],
"builtin-baseline": "1d611efb49276c65d69fb6d010c2aaaf039322b0"
}
cmake_minimum_required(VERSION 3.15)
project(LibDemo CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
add_executable(fmt_demo fmt_demo.cpp)
target_link_libraries(fmt_demo PRIVATE fmt::fmt)
add_executable(spdlog_demo spdlog_demo.cpp)
target_link_libraries(spdlog_demo PRIVATE spdlog::spdlog)
add_executable(json_demo json_demo.cpp)
target_link_libraries(json_demo PRIVATE nlohmann_json::nlohmann_json)
Конфігурація та збірка виконуються командами з лекції про інтеграцію vcpkg і CMake:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
cmake --build build
fmt: форматування рядків
Що це і навіщо
fmt — бібліотека форматування тексту, яка замінює printf і потоковий std::cout << .... Вона перевіряє типи під час компіляції (на відміну від printf), зазвичай працює швидше за потоки та має зручний синтаксис із {}. На її основі створено std::format зі стандарту C++20, а в C++23 з'явився std::print. Якщо ваш компілятор вже підтримує ці засоби, fmt усе одно залишається корисною: вона працює з C++11, має більше можливостей (кольори, форматування контейнерів) і не залежить від версії стандартної бібліотеки.
| Підхід | Перевірка типів | Читабельність | Потрібний стандарт |
|---|---|---|---|
printf("%d", x) | Немає: неправильний специфікатор дає невизначену поведінку | Середня | Будь-який |
std::cout << x | Є | Громіздка при складному форматі | Будь-який |
fmt::print("{}", x) | Є, під час компіляції | Висока | C++11 і вище |
std::format, std::print | Є | Висока | C++20 / C++23 |
Приклад використання
#include <fmt/core.h>
#include <fmt/format.h>
#include <fmt/color.h>
#include <fmt/ranges.h>
#include <string>
#include <vector>
int main() {
// Базове форматування
std::string name = "Іван";
int age = 25;
fmt::print("Привіт, {}! Тобі {} років.\n", name, age);
// Форматування з позиціями
fmt::print("{1} має {0} років\n", age, name);
// Форматування чисел
double pi = 3.14159265359;
fmt::print("π = {:.2f}\n", pi);
fmt::print("π = {:.5f}\n", pi);
// Вирівнювання
fmt::print("{:>10} | {:^10} | {:<10}\n", "Right", "Center", "Left");
// Кольоровий вивід
fmt::print(fg(fmt::color::green), "Успіх!\n");
fmt::print(fg(fmt::color::red) | fmt::emphasis::bold, "Помилка!\n");
// Форматування контейнерів
std::vector<int> numbers = {1, 2, 3, 4, 5};
fmt::print("Числа: {}\n", numbers);
// Створення рядка
std::string message = fmt::format("Рахунок: {}/100", 85);
fmt::print("{}\n", message);
// Системи числення та заповнення
fmt::print("{:#x} {:#b} {:08.3f} {:+d}\n", 255, 5, 3.14159, 7);
}
Мова специфікаторів формату
Усередині {} після двокрапки записується специфікатор у порядку заповнювач, вирівнювання, знак, #, 0, ширина, .точність, тип:
| Специфікатор | Значення | Приклад | Результат |
|---|---|---|---|
{} | Значення за замовчуванням | {} для 25 | 25 |
{1}, {0} | Позиційний індекс аргументу | {1} має {0} | імена змінюють порядок |
{:.2f} | Дробове число з двома знаками після коми | {:.2f} для 3.14159 | 3.14 |
{:>10}, {:<10}, {:^10} | Вирівнювання вправо, вліво, по центру в 10 позиціях | {:>10} для Right | Right |
{:08.3f} | Заповнення нулями, ширина 8, три знаки | {:08.3f} для 3.14159 | 0003.142 |
{:#x}, {:#b} | Шістнадцяткова, двійкова система з префіксом | {:#x} для 255 | 0xff |
{:+d} | Завжди показувати знак | {:+d} для 7 | +7 |
Власний тип у fmt
Щоб передати fmt власну структуру, потрібно описати, як її форматувати. Для цього спеціалізується шаблон fmt::formatter:
#include <fmt/format.h>
#include <string>
struct Point {
int x, y;
};
// Додаємо підтримку власного типу: у {} можна виводити Point
template <>
struct fmt::formatter<Point> : fmt::formatter<std::string> {
auto format(const Point& p, fmt::format_context& ctx) const {
return fmt::formatter<std::string>::format(fmt::format("({}, {})", p.x, p.y), ctx);
}
};
int main() {
fmt::print("{}\n", Point{1, 2});
fmt::print("[{:>10}]\n", Point{3, 4});
}
Структура наслідує форматер рядка, тому підтримує все, що вміє std::string: ширину, вирівнювання та заповнювач. Рядок {:>10} без додаткового коду вирівняв точку вправо.
Чому fmt, а не потоки
- Безпечніше. Помилка в кількості аргументів або типі виявляється під час компіляції.
- Швидше.
fmtзазвичай обганяєiostream, особливо при великій кількості виводів. Порівняння швидкості залежить від платформи й обсягу даних, тому під конкретну задачу варто вимірювати. - Зрозуміліше. Усе речення в одному рядку, без ланцюжка
<<. - Unicode. Корректно працює з UTF-8.
spdlog: логування
Що це і навіщо
Журнал подій (log) — це запис того, що відбувається в програмі. Коли програма працює на сервері, у відладчик не зазирнеш, і єдине джерело інформації — записані повідомлення. spdlog — швидка бібліотека логування. Вона використовує fmt для форматування, тож синтаксис повідомлень такий самий.
Рівні логування
Кожне повідомлення має рівень важливості. Налаштувавши мінімальний рівень, ви вирішуєте, які повідомлення з'являться:
| Рівень | Виклик | Призначення |
|---|---|---|
trace | spdlog::trace(...) | Найдрібніша деталізація, лише під час відладки |
debug | spdlog::debug(...) | Діагностична інформація |
info | spdlog::info(...) | Звичайні події (запуск, завершення) |
warn | spdlog::warn(...) | Щось незвичне, але програма працює |
error | spdlog::error(...) | Помилка, операція не виконана |
critical | spdlog::critical(...) | Серйозний збій |
За замовчуванням показуються повідомлення рівня info і вище.
Приклад використання
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
int main() {
// Базове логування
spdlog::info("Програма запущена");
spdlog::warn("Це попередження");
spdlog::error("Це помилка");
spdlog::debug("Debug повідомлення"); // за замовчуванням не показується
// Рівень логування
spdlog::set_level(spdlog::level::debug);
spdlog::debug("Тепер debug показується");
// Форматування (fmt всередині)
std::string user = "Іван";
int attempts = 3;
spdlog::info("Користувач {} зробив {} спроб", user, attempts);
// Логування у файл
auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/app.log");
file_logger->info("Записано у файл");
// Ротація файлів
auto rotating = spdlog::rotating_logger_mt("rotating", "logs/rotating.log", 1024 * 1024 * 5, 3);
rotating->info("З ротацією файлів");
// Консоль з кольорами
auto console = spdlog::stdout_color_mt("console");
console->info("Кольоровий вивід!");
// Власний формат
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");
spdlog::info("З власним форматом");
spdlog::flush_on(spdlog::level::info);
file_logger->flush();
}
Що показав запуск:
- Повідомлення
debugдо викликуset_levelне з'явилось, а після з'явилось. - Рівень
warnу вигляді тексту виводиться якwarning. - Пакет створив папку
logs/самостійно, хоча в коді її створення не було. - Іменовані логери (
file_logger,console) додають своє ім'я в рядок. Стандартний логер виводить без імені. - Повідомлення з власним шаблоном
[%Y-%m-%d ...]вивелось без імені логера: шаблон змінено лише для стандартного логера.
Sinks: куди йдуть повідомлення
Sink — це приймач повідомлень. Один логер може мати кілька sinks, наприклад, показувати помилки в консолі й записувати все у файл.
| Sink | Призначення | Заголовок |
|---|---|---|
stdout_color_sink | Консоль з кольорами рівнів | spdlog/sinks/stdout_color_sinks.h |
basic_file_sink | Один файл | spdlog/sinks/basic_file_sink.h |
rotating_file_sink | Файли з обмеженим розміром: при досягненні ліміту створюється наступний | spdlog/sinks/rotating_file_sink.h |
daily_file_sink | Новий файл щодня | spdlog/sinks/daily_file_sink.h |
Ротація потрібна, щоб лог не заповнив диск. Аргументи rotating_logger_mt(ім'я, файл, 5 МБ, 3) означають, що файл росте до 5 мегабайт, після чого зберігається ще до трьох попередніх копій.
Суфікс _mt у назвах (basic_logger_mt) означає multi-threaded: логер безпечний у програмах із кількома потоками. Варіант _st швидший, але лише для одного потоку.
Власний шаблон повідомлення
У рядку шаблону використовуються спеціальні символи:
| Символ | Значення |
|---|---|
%Y-%m-%d %H:%M:%S.%e | Дата, час із мілісекундами |
%l | Рівень повідомлення (info, error) |
%^ та %$ | Початок і кінець кольорової ділянки (рівень фарбується у колір) |
%n | Ім'я логера |
%t | Номер потоку |
%v | Саме повідомлення |
Кілька sinks в одному логері
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
int main() {
auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console->set_level(spdlog::level::warn); // у консолі лише попередження й вище
auto file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/all.log", true);
file->set_level(spdlog::level::trace); // у файлі все
spdlog::logger logger("app", {console, file});
logger.set_level(spdlog::level::trace);
logger.info("Це потрапить лише у файл");
logger.error("Це побачимо і в консолі, і у файлі");
}
Кожен sink має власний мінімальний рівень, тому в консоль потрапляє лише важливе, а файл містить повну історію.
spdlog::async_logger та spdlog::init_thread_pool. Використовуйте його, коли логування стає помітним у профілі програми.Чому spdlog
- Швидкість: бібліотека орієнтована на мінімальні витрати на повідомлення.
- Гнучкі приймачі та шаблони, ротація файлів.
- Безпека в багатопотокових програмах.
- Синтаксис як у
fmt.
nlohmann/json: робота з JSON
Що це і навіщо
JSON — текстовий формат обміну даними: його використовують веб-сервіси, файли налаштувань, API. nlohmann/json — найпопулярніша бібліотека для JSON у C++. Вона header-only (весь код у заголовках), а синтаксис нагадує роботу зі словниками в Python або об'єктами в JavaScript.
Основи: створення, читання, зміна
#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
using json = nlohmann::json;
int main() {
json person = {
{"name", "Іван"},
{"age", 25},
{"city", "Київ"},
{"skills", {"C++", "Python", "JavaScript"}},
{"employed", true}
};
// Виведення з відступами
std::cout << person.dump(2) << std::endl;
// Читання значень
std::string name = person["name"];
int age = person["age"];
std::cout << "Ім'я: " << name << ", вік: " << age << std::endl;
// Перевірка існування поля
if (person.contains("email")) {
std::cout << "Email: " << person["email"] << std::endl;
} else {
std::cout << "Email не вказано" << std::endl;
}
// Додавання полів і елементів масиву
person["email"] = "ivan@example.com";
person["skills"].push_back("CMake");
for (const auto& skill : person["skills"]) {
std::cout << "Skill: " << skill << std::endl;
}
// ... продовження нижче
}
Звертає на себе увагу кілька особливостей виводу:
- Ключі впорядковані за алфавітом (
age,city,employed, ...), а не за порядком запису. Типjsonзберігає об'єкти в упорядкованому словнику. Якщо потрібен порядок вставки, використовуйтеnlohmann::ordered_json. - Рядки виводяться в лапках, коли елемент передається в
std::coutнапряму (Skill: "C++"), але без лапок, якщо спершу присвоїти його змінній типуstd::string(Ім'я: Іван). Перша форма друкує JSON-запис, друга — значення. - Конструкція
std::string name = person["name"]працює через неявне перетворення. Якщо тип поля не збігається з очікуваним, бібліотека кине винятокtype_error.
Файли і розбір рядків
// Запис у файл і читання назад
{
std::ofstream file("person.json");
file << person.dump(4);
}
std::ifstream input("person.json");
json loaded = json::parse(input);
std::cout << "З файлу: " << loaded["email"] << std::endl;
// Розбір рядка
json point_json = json::parse(R"({"x": 10, "y": 20})");
std::cout << "x: " << point_json["x"] << ", y: " << point_json["y"] << std::endl;
Метод dump(n) перетворює JSON на рядок з відступом у n пробілів (dump() без аргументу видає компактний запис в один рядок). Функція json::parse приймає рядок або потік. Сирий рядковий літерал R"(...)" зручний, бо не вимагає екранування лапок.
Власні типи: to_json і from_json
Щоб перетворювати власні структури на JSON і назад, потрібно оголосити дві функції в тому ж просторі імен, що й тип:
struct Point {
int x, y;
};
void to_json(json& j, const Point& p) {
j = json{{"x", p.x}, {"y", p.y}};
}
void from_json(const json& j, Point& p) {
j.at("x").get_to(p.x);
j.at("y").get_to(p.y);
}
// Використання
Point p = point_json.get<Point>(); // JSON -> Point
json back = Point{3, 4}; // Point -> JSON
person.get<User>() для структури User, визначеної всерединіmain, без оголошених to_json/from_json. Такий код не компілюється: бібліотека не знає, як перетворити тип, а для локальних типів механізм пошуку функцій у просторі імен (ADL) не працює. Тип має бути оголошений на рівні файлу, а обидві функції написані поряд.Обробка помилок
Некоректний JSON призводить до винятку, який потрібно перехоплювати:
try {
json bad = json::parse("{\"x\": ");
} catch (const json::parse_error& e) {
std::cout << "Помилка: " << e.what() << std::endl;
}
Повідомлення вказує рядок, стовпець і причину. Крім parse_error існують type_error (невідповідний тип), out_of_range (доступ до відсутнього ключа через at()) та інші. Усі вони наслідують nlohmann::json::exception.
Типові операції
| Задача | Код |
|---|---|
| Створити порожній об'єкт | json j = json::object(); |
| Створити масив | json a = json::array({1, 2, 3}); |
| Значення за замовчуванням | int port = j.value("port", 8080); |
| Безпечний доступ із перевіркою | j.at("key") (кидає виняток, якщо ключа немає) |
| Перебір пар «ключ-значення» | for (auto& [key, val] : j.items()) { ... } |
| Кількість елементів | j.size() |
| Видалення | j.erase("key"); |
| Порядок як при записі | nlohmann::ordered_json |
Питання для самоперевірки
printf не перевіряє відповідність специфікатора (%d) типу аргументу, і помилка проявляється як невизначена поведінка під час виконання. fmt перевіряє кількість і типи аргументів під час компіляції, а невідомий тип можна навчити форматуватися спеціалізацією fmt::formatter.info, тому debug та trace відкидаються. Поки не викликано spdlog::set_level(spdlog::level::debug), такі повідомлення не виводяться. Це дозволяє залишити в коді докладні повідомлення і вмикати їх лише за потреби.nlohmann::json зберігає об'єкти в упорядкованому за ключем словнику, тому dump() видає ключі за алфавітом. Якщо порядок важливий (наприклад, для читабельності конфігурації), замініть json на nlohmann::ordered_json.to_json та from_json через ADL, тобто пошук функції в просторі імен типу. Локальний тип (оголошений усередині функції) не може мати таких функцій на рівні файлу, тому виклик get<T>() не збереться. Структуру слід оголосити поза main.Практичне завдання
Напишіть програму notes, яка:
- Зберігає нотатки (
struct Note { int id; std::string text; }) у файліnotes.json; реалізуйтеto_jsonіfrom_json. - Виводить список нотаток таблицею через
fmtіз вирівнюванням стовпців ({:>3} | {:<30}). - Логує через
spdlog: рівеньinfoдля додавання/видалення,warnдля спроби видалити неіснуючу нотатку,errorпри пошкодженому JSON-файлі. - Пише повний журнал у
logs/notes.log(рівеньdebug), а в консоль виводить лишеwarnі вище.
- Файл може не існувати при першому запуску: перевіряйте
std::filesystem::exists. json::parseкидаєparse_error; перехопіть його й повідомте про помилку рівнемerror.- Для різних рівнів у консолі та файлі потрібен логер з двома sinks (див. приклад вище).
- Вирівнювання для кирилиці в
fmtрахується в байтах UTF-8, тому стовпці з українським текстом можуть «їхати». Для навчальної таблиці це прийнятно, а для точного вирівнювання використовують бібліотеки з підтримкою ширини Unicode.
Резюме
✍️ fmt
{}, перевірка типів під час компіляції, вирівнювання, числа, кольори, контейнери. Власний тип підключається спеціалізацією fmt::formatter.📜 spdlog
info.🧾 nlohmann/json
dump, parse, to_json/from_json для власних типів, виняток parse_error.🔗 Підключення
fmt::fmt, spdlog::spdlog, nlohmann_json::nlohmann_json після find_package(... CONFIG REQUIRED) та manifest у vcpkg.vcpkg для досвідчених: версії, features, реєстри, власні порти, кеш і діагностика
Фіксація версій через builtin-baseline, version>= та overrides, необов'язкові features, vcpkg-configuration.json із реєстрами та overlay-портами, створення власного порту, бінарний кеш, використання у CI і розв'язання типових проблем.
Бібліотеки C++: cpp-httplib, cxxopts, CLI11, Google Test і Catch2
HTTP-сервер і клієнт на cpp-httplib з JSON API, розбір аргументів командного рядка у cxxopts та CLI11, юніт-тестування в Google Test і Catch2 v3. Підключення через vcpkg, перевірені приклади та реальні виводи.