CMake і vcpkg

Бібліотеки C++: fmt, spdlog і nlohmann/json

Форматування рядків бібліотекою fmt, логування через spdlog (рівні, sinks, ротація, шаблони), робота з JSON у nlohmann/json: створення, читання, файли, власні типи, обробка помилок. Підключення через vcpkg та CMake, перевірені приклади.

Бібліотеки 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);
}
./build/fmt_demo
Привіт, Іван! Тобі 25 років.
Іван має 25 років
π = 3.14
π = 3.14159
Right | Center | Left
Успіх!
Помилка!
Числа: [1, 2, 3, 4, 5]
Рахунок: 85/100
0xff 0b101 0003.142 +7

Мова специфікаторів формату

Усередині {} після двокрапки записується специфікатор у порядку заповнювач, вирівнювання, знак, #, 0, ширина, .точність, тип:

СпецифікаторЗначенняПрикладРезультат
{}Значення за замовчуванням{} для 2525
{1}, {0}Позиційний індекс аргументу{1} має {0}імена змінюють порядок
{:.2f}Дробове число з двома знаками після коми{:.2f} для 3.141593.14
{:>10}, {:<10}, {:^10}Вирівнювання вправо, вліво, по центру в 10 позиціях{:>10} для Right Right
{:08.3f}Заповнення нулями, ширина 8, три знаки{:08.3f} для 3.141590003.142
{:#x}, {:#b}Шістнадцяткова, двійкова система з префіксом{:#x} для 2550xff
{:+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});
}
./build/fmt_custom
(1, 2)
[ (3, 4)]

Структура наслідує форматер рядка, тому підтримує все, що вміє std::string: ширину, вирівнювання та заповнювач. Рядок {:>10} без додаткового коду вирівняв точку вправо.

Чому fmt, а не потоки

  • Безпечніше. Помилка в кількості аргументів або типі виявляється під час компіляції.
  • Швидше. fmt зазвичай обганяє iostream, особливо при великій кількості виводів. Порівняння швидкості залежить від платформи й обсягу даних, тому під конкретну задачу варто вимірювати.
  • Зрозуміліше. Усе речення в одному рядку, без ланцюжка <<.
  • Unicode. Корректно працює з UTF-8.

spdlog: логування

Що це і навіщо

Журнал подій (log) — це запис того, що відбувається в програмі. Коли програма працює на сервері, у відладчик не зазирнеш, і єдине джерело інформації — записані повідомлення. spdlog — швидка бібліотека логування. Вона використовує fmt для форматування, тож синтаксис повідомлень такий самий.

Рівні логування

Кожне повідомлення має рівень важливості. Налаштувавши мінімальний рівень, ви вирішуєте, які повідомлення з'являться:

РівеньВикликПризначення
tracespdlog::trace(...)Найдрібніша деталізація, лише під час відладки
debugspdlog::debug(...)Діагностична інформація
infospdlog::info(...)Звичайні події (запуск, завершення)
warnspdlog::warn(...)Щось незвичне, але програма працює
errorspdlog::error(...)Помилка, операція не виконана
criticalspdlog::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();
}
./build/spdlog_demo
[2026-11-07 09:25:05.798] [info] Програма запущена
[2026-11-07 09:25:05.798] [warning] Це попередження
[2026-11-07 09:25:05.798] [error] Це помилка
[2026-11-07 09:25:05.798] [debug] Тепер debug показується
[2026-11-07 09:25:05.798] [info] Користувач Іван зробив 3 спроб
[2026-11-07 09:25:05.798] [console] [info] Кольоровий вивід!
[2026-11-07 09:25:05.798] [info] З власним форматом
cat logs/app.log
[2026-11-07 09:25:05.798] [file_logger] [info] Записано у файл

Що показав запуск:

  • Повідомлення 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;
    }
    // ... продовження нижче
}
./build/json_demo (початок)
{
"age": 25,
"city": "Київ",
"employed": true,
"name": "Іван",
"skills": [
"C++",
"Python",
"JavaScript"
]
}
Ім'я: Іван, вік: 25
Email не вказано
Skill: "C++"
Skill: "Python"
Skill: "JavaScript"
Skill: "CMake"

Звертає на себе увагу кілька особливостей виводу:

  • Ключі впорядковані за алфавітом (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
./build/json_demo (продовження)
З файлу: "ivan@example.com"
x: 10, y: 20
Point: 10, 20
Point -> {"x":3,"y":4}
У поширених прикладах зустрічається виклик 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;
    }
результат
Помилка: [json.exception.parse_error.101] parse error at line 1, column 7: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal

Повідомлення вказує рядок, стовпець і причину. Крім 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

Питання для самоперевірки

Практичне завдання

Резюме

✍️ fmt

Форматування з {}, перевірка типів під час компіляції, вирівнювання, числа, кольори, контейнери. Власний тип підключається спеціалізацією fmt::formatter.

📜 spdlog

Рівні, sinks (консоль, файл, ротація), іменовані логери та шаблон повідомлення. Рівень за замовчуванням info.

🧾 nlohmann/json

JSON як словник: створення, читання, dump, parse, to_json/from_json для власних типів, виняток parse_error.

🔗 Підключення

fmt::fmt, spdlog::spdlog, nlohmann_json::nlohmann_json після find_package(... CONFIG REQUIRED) та manifest у vcpkg.
Далі розглянемо бібліотеки для мережі та командного рядка (cpp-httplib, cxxopts, CLI11), а також тестові фреймворки Google Test і Catch2.
Copyright © 2026