Cmake

Boost.Optional, Boost.Variant и их аналоги в современном C++: нужен ли Boost вообще

Введение

Долгое время Boost был фактическим «полигоном» для будущих возможностей стандартной библиотеки C++ — многие классы, такие как optional, variant, any, filesystem, появились в Boost задолго до их стандартизации. С приходом C++17 и C++20 значительная часть этого функционала вошла в стандартную библиотеку как std::optional, std::variant, std::any, std::filesystem. В статье разберём, остаётся ли смысл использовать Boost-версии этих компонентов в новых Qt-проектах, ориентированных на современный стандарт.

Концепция

std::optional и boost::optional решают одну и ту же задачу — представление значения, которое может быть отсутствующим, без использования указателей или специальных «магических» значений. Для нового кода, ориентированного на C++17 и новее, std::optional предпочтительнее: он часть стандарта, лучше известен сообществу, и не добавляет зависимость от Boost. То же касается std::variant против boost::variant и std::any против boost::any.

Boost-версии всё ещё оправданы в нескольких случаях: (1) проект должен поддерживать компиляторы или стандарты старше C++17; (2) нужна функциональность, которой нет в стандартной версии (например, boost::variant2 с иной моделью исключений, или boost::optional с поддержкой ссылочных типов, которой нет в std::optional); (3) интеграция со сторонним кодом, уже завязанным на Boost-типы.

Пример кода: Boost.Optional, Boost.Variant и

#include <optional>
#include <variant>
#include <iostream>
#include <string>

// std::optional — стандартная альтернатива boost::optional с C++17
std::optional<int> parseNumber(const std::string &text)
{
    try {
        return std::stoi(text);
    } catch (const std::exception &) {
        return std::nullopt;
    }
}

// std::variant — стандартная альтернатива boost::variant с C++17
using Shape = std::variant<int /* радиус круга */, std::pair<int, int> /* стороны прямоугольника */>;

double computeArea(const Shape &shape)
{
    if (std::holds_alternative<int>(shape)) {
        int radius = std::get<int>(shape);
        return 3.14159 * radius * radius;
    } else {
        auto [width, height] = std::get<std::pair<int, int>>(shape);
        return width * height;
    }
}

int main()
{
    auto result = parseNumber("42");
    if (result.has_value()) {
        std::cout << "Распознано число: " << *result << std::endl;
    }

    auto invalidResult = parseNumber("not_a_number");
    std::cout << "Результат разбора некорректной строки: "
              << invalidResult.value_or(-1) << std::endl;

    Shape circle = 5;
    Shape rectangle = std::make_pair(4, 6);

    std::cout << "Площадь круга: " << computeArea(circle) << std::endl;
    std::cout << "Площадь прямоугольника: " << computeArea(rectangle) << std::endl;

    return 0;
}

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

parseNumber демонстрирует типичное применение std::optional — функция, которая может не вернуть результат (некорректный ввод), без выброса исключения наружу и без использования специального «магического» значения вроде -1, которое могло бы быть валидным результатом в другом контексте. value_or() даёт удобный способ подставить значение по умолчанию при отсутствии результата.

Shape на основе std::variant показывает типобезопасную альтернативу классическому union или иерархии классов с виртуальными методами для простых случаев «это один из нескольких заранее известных типов» — std::holds_alternative и std::get дают безопасный доступ к текущему хранимому типу.

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

  • Слепой перенос старого кода с Boost на std без проверки отличий в API. std::optional не поддерживает ссылочные типы (std::optional<T&> не специализирован стандартно так, как boost::optional<T&>), и некоторые тонкости поведения при копировании/перемещении различаются — механическая замена boost:: на std:: не всегда компилируется без изменений.
  • Версия компилятора/стандарта проекта может быть зафиксирована на C++11/14 по историческим причинам (старый embedded toolchain, устаревшие зависимости) — в таких случаях std::optional/std::variant просто недоступны, и Boost остаётся единственным вариантом без серьёзного рефакторинга toolchain.
  • std::variant при пустом/невалидном состоянии (после исключения при перемещении) уходит в std::monostate/valueless_by_exception — поведение, отличающееся от boost::variant, которое в более старых версиях гарантировало «никогда не быть пустым» за счёт дополнительных накладных расходов на резервное хранение.
  • Сохранение зависимости от Boost только из инерции, когда весь функционал, который реально используется, уже покрывается стандартной библиотекой — лишняя зависимость увеличивает время сборки и поверхность для уязвимостей без реальной выгоды.