CMake і vcpkg

Бібліотеки C++: cpp-httplib, cxxopts, CLI11, Google Test і Catch2

HTTP-сервер і клієнт на cpp-httplib з JSON API, розбір аргументів командного рядка у cxxopts та CLI11, юніт-тестування в Google Test і Catch2 v3. Підключення через vcpkg, перевірені приклади та реальні виводи.

Бібліотеки C++: cpp-httplib, cxxopts, CLI11, Google Test і Catch2

🎯 Мета лекції

  • Написати HTTP-сервер і клієнт на cpp-httplib та підключити відповідь у форматі JSON.
  • Розбирати аргументи командного рядка бібліотеками cxxopts і CLI11 та порівняти їх.
  • Писати тести у Google Test та Catch2 і запускати їх через CTest.
  • Правильно підключати кожну бібліотеку через vcpkg і CMake.

🔑 Ключові терміни

  • Маршрут (route): шлях запиту (/api/user), для якого зареєстрований обробник.
  • Обробник (handler): функція, що виконується при запиті на маршрут.
  • Опція командного рядка: параметр виду --file x або -v.
  • Тест-кейс (test case): незалежна перевірка однієї можливості.

Попередня лекція розглядала бібліотеки загального призначення. Тут розглядаються три прикладні задачі: обмін даними по мережі, керування програмою з терміналу та автоматична перевірка коду. Усі приклади зібрано і запущено в одному vcpkg-проєкті.

Підготовка проєкту

{
  "name": "lib-demo2",
  "version": "1.0.0",
  "dependencies": ["cpp-httplib", "nlohmann-json", "cxxopts", "cli11", "catch2", "gtest"],
  "builtin-baseline": "1d611efb49276c65d69fb6d010c2aaaf039322b0"
}
cmake_minimum_required(VERSION 3.15)
project(LibDemo2 CXX)
set(CMAKE_CXX_STANDARD 17)

find_package(httplib CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(cxxopts CONFIG REQUIRED)
find_package(CLI11 CONFIG REQUIRED)
find_package(Catch2 CONFIG REQUIRED)

add_executable(server server.cpp)
target_link_libraries(server PRIVATE httplib::httplib nlohmann_json::nlohmann_json)

add_executable(client client.cpp)
target_link_libraries(client PRIVATE httplib::httplib)

# macOS: cpp-httplib використовує системний резолвер Apple
if(APPLE)
    target_link_libraries(server PRIVATE "-framework CoreFoundation" "-framework CFNetwork")
    target_link_libraries(client PRIVATE "-framework CoreFoundation" "-framework CFNetwork")
endif()

add_executable(cli11_demo cli11_demo.cpp)
target_link_libraries(cli11_demo PRIVATE CLI11::CLI11)

add_executable(cxxopts_demo cxxopts_demo.cpp)
target_link_libraries(cxxopts_demo PRIVATE cxxopts::cxxopts)

add_executable(catch_tests catch_tests.cpp)
target_link_libraries(catch_tests PRIVATE Catch2::Catch2WithMain)

enable_testing()
include(CTest)
include(Catch)
catch_discover_tests(catch_tests)
Блок if(APPLE) з'явився не випадково. Версія 0.27.0 cpp-httplib у vcpkg на macOS викликає системний резолвер імен Apple (CFHost), але ціль httplib::httplib не додає потрібних фреймворків. Без них лінкер завершується помилкою «Undefined symbols ... _CFArrayGetCount». Це приклад реальної поведінки пакетів: якщо збірка падає при лінкуванні, дивіться перші рядки помилки: вони називають відсутні символи, а за назвою можна зрозуміти, яку бібліотеку підключити. На Windows і Linux додаткових налаштувань не потрібно.

cpp-httplib: HTTP без зайвого

Що це і навіщо

HTTP — протокол, за яким спілкуються браузери й веб-сервіси. cpp-httplib реалізує сервер і клієнт одним заголовковим файлом: без залежностей, без складного налаштування. Вона підходить для невеликих API, службових серверів і тестових стендів. Для висококонкурентних промислових сервісів використовують потужніші рішення (Boost.Beast, Drogon), але для навчання та прототипів cpp-httplib ідеальна.

Сервер

#include <httplib.h>
#include <nlohmann/json.hpp>
#include <iostream>

int main() {
    httplib::Server svr;

    // GET запит
    svr.Get("/", [](const httplib::Request&, httplib::Response& res) {
        res.set_content("Привіт від HTTP сервера!", "text/plain; charset=utf-8");
    });

    // GET з параметром шляху
    svr.Get("/hello/:name", [](const httplib::Request& req, httplib::Response& res) {
        std::string name = req.path_params.at("name");
        res.set_content("Привіт, " + name + "!", "text/plain; charset=utf-8");
    });

    // POST запит
    svr.Post("/api/data", [](const httplib::Request& req, httplib::Response& res) {
        std::cout << "Отримано: " << req.body << std::endl;
        res.set_content("OK", "text/plain");
    });

    // JSON API
    svr.Get("/api/user", [](const httplib::Request&, httplib::Response& res) {
        nlohmann::json user = {{"name", "Іван"}, {"age", 25}};
        res.set_content(user.dump(), "application/json");
    });

    // Зупинка сервера для демонстрації
    svr.Get("/stop", [&](const httplib::Request&, httplib::Response& res) {
        res.set_content("bye", "text/plain");
        svr.stop();
    });

    std::cout << "Сервер запущено на http://localhost:8765" << std::endl;
    svr.listen("localhost", 8765);
    std::cout << "Сервер зупинено" << std::endl;
}

Структура проста: об'єкт httplib::Server, до якого додаються обробники методами Get, Post, Put, Delete. Обробник — лямбда-функція з двома параметрами: запит Request (тут шлях, тіло, заголовки, параметри) та відповідь Response (її заповнює функція). Виклик listen блокує потік і обслуговує запити, доки сервер не зупинять.

Фрагмент :name у шляху /hello/:name — параметр маршруту. Значення читається з req.path_params.

Клієнт

#include <httplib.h>
#include <iostream>

int main() {
    httplib::Client cli("http://localhost:8765");

    auto res = cli.Get("/");
    if (res && res->status == 200) {
        std::cout << "GET /            -> " << res->status << " " << res->body << std::endl;
    }

    res = cli.Get("/hello/Оксана");
    if (res) {
        std::cout << "GET /hello/...   -> " << res->status << " " << res->body << std::endl;
    }

    res = cli.Get("/api/user");
    if (res) {
        std::cout << "GET /api/user    -> " << res->status << " " << res->body
                  << " (" << res->get_header_value("Content-Type") << ")" << std::endl;
    }

    auto post = cli.Post("/api/data", R"({"key": "value"})", "application/json");
    if (post) {
        std::cout << "POST /api/data   -> " << post->status << " " << post->body << std::endl;
    }

    res = cli.Get("/missing");
    if (res) {
        std::cout << "GET /missing     -> " << res->status << std::endl;
    }

    cli.Get("/stop");
}

Запит повертає Result: об'єкт, який приводиться до bool. Значення false означає, що з'єднатися не вдалося (сервер недоступний, обрив мережі), і тоді res->status читати не можна. Тому перевірка if (res) обов'язкова перед зверненням до статусу. Код 404 для невідомого маршруту — це успішно отримана відповідь, res для нього істинний.

Результат одночасного запуску сервера і клієнта:

клієнт та сервер
$ ./build/server &
$ ./build/client
GET / -> 200 Привіт від HTTP сервера!
GET /hello/... -> 200 Привіт, Оксана!
GET /api/user -> 200 {"age":25,"name":"Іван"} (application/json)
POST /api/data -> 200 OK
GET /missing -> 404
(вивід сервера)
Сервер запущено на http://localhost:8765
Отримано: {"key": "value"}
Сервер зупинено

Сервер сумісний зі стандартними інструментами. Той самий запит можна надіслати утилітою curl:

curl -i localhost:8765/api/user
$ curl -i localhost:8765/api/user
HTTP/1.1 200 OK
Content-Type: application/json
Keep-Alive: timeout=5, max=100
Content-Length: 28
{"age":25,"name":"Іван"}
Для HTTPS бібліотеці потрібен OpenSSL. У vcpkg це окрема можливість (feature): { "name": "cpp-httplib", "features": ["openssl"] }, а в коді перед підключенням заголовка визначається макрос CPPHTTPLIB_OPENSSL_SUPPORT. Подробиці про features були в лекції про просунуті можливості vcpkg.
Сервер із прикладу слухає адресу localhost, тобто доступний лише з вашого комп'ютера, і не містить автентифікації. Перш ніж виставляти подібний сервіс у мережу, потрібно додати перевірку вхідних даних, обмеження розміру запитів, HTTPS і авторизацію. cpp-httplib є зручним інструментом для навчання і внутрішніх сервісів, але не готовим захищеним сервером.

Аргументи командного рядка

Програми, які запускаються з терміналу, приймають параметри: --file data.txt --verbose. Розбирати argv вручну незручно: потрібно підтримувати довгі й короткі форми, значення за замовчуванням, довідку, повідомлення про помилки. Цю роботу беруть на себе бібліотеки.

cxxopts

Легка header-only бібліотека зі стилем, схожим на GNU-утиліти:

#include <cxxopts.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    cxxopts::Options options("MyApp", "Опис програми");

    options.add_options()
        ("f,file", "Input file", cxxopts::value<std::string>())
        ("o,output", "Output file", cxxopts::value<std::string>()->default_value("output.txt"))
        ("v,verbose", "Verbose output", cxxopts::value<bool>()->default_value("false"))
        ("c,count", "Repeat count", cxxopts::value<int>()->default_value("1"))
        ("h,help", "Print help");

    try {
        auto result = options.parse(argc, argv);

        if (result.count("help")) {
            std::cout << options.help() << std::endl;
            return 0;
        }

        if (result.count("file")) {
            std::cout << "Input file: " << result["file"].as<std::string>() << std::endl;
        }

        std::cout << "Output: " << result["output"].as<std::string>() << std::endl;
        std::cout << "Verbose: " << (result["verbose"].as<bool>() ? "yes" : "no") << std::endl;
        std::cout << "Count: " << result["count"].as<int>() << std::endl;
    } catch (const cxxopts::exceptions::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

Запис ("f,file", "опис", тип) визначає одразу коротку (-f) та довгу (--file) форму. Значення за замовчуванням задається через ->default_value. Метод count("ключ") повідомляє, чи вказав користувач опцію.

cxxopts_demo
$ ./build/cxxopts_demo --file input.txt -o result.txt -v -c 5
Input file: input.txt
Output: result.txt
Verbose: yes
Count: 5
$ ./build/cxxopts_demo --count abc
Error: Argument 'abc' failed to parse
$ ./build/cxxopts_demo --help
Опис програми
Usage:
MyApp [OPTION...]
-f, --file arg Input file
-o, --output arg Output file (default: output.txt)
-v, --verbose Verbose output
-c, --count arg Repeat count (default: 1)
-h, --help Print help

Довідку --help бібліотека склала самостійно. Некоректне значення (abc замість числа) призводить до виняткової ситуації, яку код перехоплює й повідомляє про помилку.

CLI11

Альтернатива з багатшими можливостями: перевірки значень (валідатори), підкоманди (як у git commit, git push), обов'язкові параметри.

#include <CLI/CLI.hpp>
#include <iostream>
#include <string>

int main(int argc, char** argv) {
    CLI::App app{"Демонстрація CLI11"};

    std::string file;
    int count = 1;
    bool verbose = false;

    app.add_option("-f,--file", file, "Вхідний файл")->required();
    app.add_option("-c,--count", count, "Кількість повторів")->check(CLI::PositiveNumber);
    app.add_flag("-v,--verbose", verbose, "Докладний вивід");

    CLI11_PARSE(app, argc, argv);

    std::cout << "file=" << file << " count=" << count << " verbose=" << verbose << std::endl;
    return 0;
}

Відмінність підходу: значення записуються прямо у ваші змінні (file, count), без звернення до таблиці результатів. Метод ->required() робить параметр обов'язковим, а ->check(CLI::PositiveNumber) застосовує перевірку.

cli11_demo
$ ./build/cli11_demo -f a.txt -c 3 -v
file=a.txt count=3 verbose=1
$ ./build/cli11_demo -c 0 -f a.txt
--count: Value 0 not in range [2.22507e-308 - 1.79769e+308]
Run with --help for more information.
$ ./build/cli11_demo
--file is required
Run with --help for more information.

Валідатор відхилив нуль і пояснив причину, а пропущений обов'язковий параметр викликав окреме повідомлення. Усе це відбулося без жодного рядка власного коду перевірки.

Порівняння

ХарактеристикаcxxoptsCLI11
ПідхідТаблиця результатів result["ключ"]Прив'язка до змінних
ПідкомандиНемаєЄ
ВалідаториНемає, лише розбір типуЄ (PositiveNumber, діапазони, файли)
Обов'язкові параметриПеревіряються вручну->required()
Розмір і складністьМенше, простішийБільше можливостей
Підключенняcxxopts::cxxoptsCLI11::CLI11

Для невеликої утиліти з кількома опціями достатньо cxxopts. Якщо потрібні підкоманди або перевірки значень, зручніший CLI11.

Тестові фреймворки

Основи тестування й CTest розглядались у лекції про тестування. Тут показано, як підключити тестові фреймворки через vcpkg та порівняти два найпоширеніші.

Google Test через vcpkg

У вихідній лекції Google Test завантажувався FetchContent. Менеджер пакетів дозволяє не завантажувати його при кожній конфігурації:

{ "dependencies": ["gtest"] }
find_package(GTest CONFIG REQUIRED)

add_executable(math_tests tests/test_math.cpp)
target_link_libraries(math_tests PRIVATE GTest::gtest_main MathUtils)

include(GoogleTest)
gtest_discover_tests(math_tests)

Сам код тестів (TEST, EXPECT_EQ, параметризовані тести) залишається тим самим.

Catch2 v3

Catch2 — фреймворк із простішим синтаксисом: тести оголошуються макросом TEST_CASE, а допоміжні підрозділи SECTION дозволяють ділити тест на частини з окремим налаштуванням.

#include <catch2/catch_test_macros.hpp>
#include <vector>

int add(int a, int b) {
    return a + b;
}

TEST_CASE("Addition works correctly", "[math]") {
    REQUIRE(add(2, 2) == 4);
    REQUIRE(add(-1, 1) == 0);
    REQUIRE(add(0, 0) == 0);
}

TEST_CASE("Vectors work", "[containers]") {
    std::vector<int> v;

    SECTION("starts empty") {
        REQUIRE(v.empty());
        REQUIRE(v.size() == 0);
    }

    SECTION("can add elements") {
        v.push_back(5);
        REQUIRE(v.size() == 1);
        REQUIRE(v[0] == 5);
    }
}
У старих прикладах Catch2 використовують #define CATCH_CONFIG_MAIN та заголовок <catch2/catch.hpp>. Так працювала версія 2. У версії 3, яку встановлює vcpkg, цього заголовка немає: підключається <catch2/catch_test_macros.hpp>, а функцію main надає ціль Catch2::Catch2WithMain, а не макрос. Копіювання коду зі старих статей призводить до помилки «file not found».

Для CTest Catch2 має власну функцію реєстрації catch_discover_tests (підключається include(Catch)):

catch_tests та ctest
$ ./build/catch_tests
Randomness seeded to: 3663595909
===============================================================================
All tests passed (7 assertions in 2 test cases)
$ cd build && ctest
Start 1: Vectors work
1/2 Test #1: Vectors work ..................... Passed 0.01 sec
Start 2: Addition works correctly
2/2 Test #2: Addition works correctly ......... Passed 0.00 sec
100% tests passed, 0 tests failed out of 2

Повідомлення «7 assertions in 2 test cases» показує, що секції SECTION не збільшили кількість тест-кейсів. Порядок запуску вибирається випадково (Randomness seeded): це допомагає виявляти тести, які залежать від виконання один одного.

Google Test чи Catch2

ХарактеристикаGoogle TestCatch2 v3
Оголошення тестуTEST(Набір, Ім'я)TEST_CASE("опис", "[теги]")
ПеревіркиEXPECT_* (продовжує), ASSERT_* (зупиняє)CHECK (продовжує), REQUIRE (зупиняє)
Спільне налаштуванняКласи-фікстуриSECTION усередині тесту
Параметризовані тестиTEST_P, INSTANTIATE_TEST_SUITE_PGENERATE(...)
Імена тестівІдентифікатори C++Довільні рядки
Макроси мокуванняУ складі (gMock)Немає, використовують окрему бібліотеку
Цілі CMakeGTest::gtest_mainCatch2::Catch2WithMain

Якщо потрібне мокування (підміна залежностей у тестах), вибирають Google Test з gMock. Для швидких тестів із читабельними описами зручніший Catch2. Обидва фреймворки однаково працюють із CTest, тому перевага визначається смаком команди.

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

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

Резюме

🌐 cpp-httplib

Сервер і клієнт в одному заголовку: Get, Post, параметри шляху, JSON. Перед читанням відповіді перевіряйте if (res). На macOS додайте системні фреймворки.

⌨️ cxxopts і CLI11

cxxopts легший, CLI11 має підкоманди та валідатори. Обидві складають довідку --help автоматично.

🧪 Google Test і Catch2

Обидва інтегруються в CTest (gtest_discover_tests, catch_discover_tests). Catch2 v3 використовує catch_test_macros.hpp і Catch2WithMain.
Далі: файлова система, математика, бази даних і формати конфігурацій. Це std::filesystem, Eigen, SQLite, YAML і TOML, а також огляд Boost.
Copyright © 2026