std::variant в C++17: безопасная альтернатива union с практическими примерами

std::variant в C++17: безопасная альтернатива union с практическими примерами

std::variant в C++17: безопасная альтернатива union с практическими примерами

std::variant в C++17 — это контейнер из стандартной библиотеки, который в каждый момент времени хранит значение ровно одного типа из заданного списка. Проще говоря, переменная может быть либо int, либо std::string, либо, например, double, но не всем сразу.

Поисковый запрос, под который полезна эта статья: «std::variant C++17 примеры». Если вы уже знаете основы классов, функций и шаблонов, std::variant станет отличным следующим шагом к более безопасному современному C++.

Зачем нужен std::variant

Представьте, что программа читает настройки из файла. Значение настройки может быть числом, строкой или логическим значением:

  • max_users = 100
  • app_name = "Demo"
  • debug = true
  • Можно хранить всё строками, но тогда придётся постоянно вручную преобразовывать типы. Можно использовать наследование, но это часто слишком сложно для простой задачи. std::variant решает проблему аккуратно: список возможных типов известен заранее, а компилятор помогает не ошибиться.

    Подключение и базовый пример

    std::variant появился в C++17, поэтому компилировать код нужно с соответствующим стандартом: -std=c++17 или новее.

    #include <iostream>
    #include <string>
    #include <variant>
    
    int main() {
        std::variant<int, std::string, double> value;
    
        value = 10;
        value = std::string("Hello");
        value = 3.14;
    
        std::cout << "Индекс текущего типа: " << value.index() << std::endl;
    }
    

    Метод index() возвращает номер текущего типа в списке. Для std::variant<int, std::string, double>: int имеет индекс 0, std::string — 1, double — 2.

    Как безопасно получить значение

    Для чтения значения используется std::get. Но важно: если запросить не тот тип, будет выброшено исключение std::bad_variant_access.

    #include <iostream>
    #include <string>
    #include <variant>
    
    int main() {
        std::variant<int, std::string> data = std::string("C++");
    
        try {
            std::cout << std::get<std::string>(data) << std::endl;
            std::cout << std::get<int>(data) << std::endl; // ошибка
        } catch (const std::bad_variant_access& e) {
            std::cout << "Неверный тип внутри variant" << std::endl;
        }
    }
    

    Такой код работает, но в реальных проектах лучше сначала проверить тип.

    Проверка типа через std::holds_alternative

    std::holds_alternative<T> возвращает true, если внутри variant сейчас хранится тип T.

    #include <iostream>
    #include <string>
    #include <variant>
    
    int main() {
        std::variant<int, std::string, bool> setting = true;
    
        if (std::holds_alternative<bool>(setting)) {
            std::cout << "Boolean: " << std::get<bool>(setting) << std::endl;
        }
    
        setting = std::string("admin");
    
        if (std::holds_alternative<std::string>(setting)) {
            std::cout << "String: " << std::get<std::string>(setting) << std::endl;
        }
    }
    

    Это понятный вариант для новичков: проверили тип — получили значение.

    std::get_if: получение без исключений

    Если вы не хотите работать с исключениями, используйте std::get_if. Он возвращает указатель на значение или nullptr, если тип не совпал.

    #include <iostream>
    #include <string>
    #include <variant>
    
    int main() {
        std::variant<int, std::string> result = 404;
    
        if (int* code = std::get_if<int>(&result)) {
            std::cout << "Код ошибки: " << *code << std::endl;
        }
    
        if (std::string* text = std::get_if<std::string>(&result)) {
            std::cout << "Текст: " << *text << std::endl;
        } else {
            std::cout << "Это не строка" << std::endl;
        }
    }
    

    std::get_if часто удобен в прикладном коде: нет исключений, логика проверки явно видна.

    std::visit: правильная обработка всех вариантов

    Самый мощный способ работать с std::variant — std::visit. Он вызывает функцию для текущего значения, независимо от того, какого оно типа.

    #include <iostream>
    #include <string>
    #include <variant>
    
    int main() {
        std::variant<int, std::string, double> value = std::string("price");
    
        std::visit([](const auto& item) {
            std::cout << "Значение: " << item << std::endl;
        }, value);
    }
    

    Здесь лямбда принимает const auto&, поэтому подходит для любого типа внутри variant. Это удобно, когда для всех типов можно выполнить одно и то же действие, например вывести значение.

    Практический пример: результат операции

    Частая задача: функция должна вернуть либо успешный результат, либо ошибку. Для этого можно использовать std::variant.

    #include <iostream>
    #include <string>
    #include <variant>
    
    struct Error {
        std::string message;
    };
    
    using ParseResult = std::variant<int, Error>;
    
    ParseResult parseNumber(const std::string& text) {
        try {
            return std::stoi(text);
        } catch (...) {
            return Error{"Не удалось преобразовать строку в число"};
        }
    }
    
    int main() {
        ParseResult result = parseNumber("123abc");
    
        if (int* number = std::get_if<int>(&result)) {
            std::cout << "Число: " << *number << std::endl;
        } else if (Error* error = std::get_if<Error>(&result)) {
            std::cout << "Ошибка: " << error->message << std::endl;
        }
    }
    

    В этом примере тип результата явно показывает: функция может вернуть либо int, либо Error. Это читается лучше, чем магические значения вроде -1 или пустой строки.

    std::variant или union?

    union тоже позволяет хранить разные типы в одной области памяти, но он гораздо менее безопасен. Программист сам должен помнить, какой тип сейчас активен. Ошибка легко приводит к неопределённому поведению.

    std::variant хранит информацию об активном типе и предоставляет безопасные инструменты: std::get, std::get_if, std::holds_alternative, std::visit. Поэтому для обычного прикладного кода почти всегда лучше выбирать std::variant.

    Типичные ошибки новичков

  • Запрашивать не тот тип через std::get. Если не уверены в типе, используйте std::holds_alternative или std::get_if.
  • Добавлять слишком много типов. Если в variant уже 10-15 вариантов, возможно, стоит пересмотреть архитектуру.
  • Использовать variant вместо обычной структуры. Если объект всегда содержит несколько полей одновременно, нужен struct, а не variant.
  • Путать variant и optional. std::optional<T> — это «значение есть или нет», а std::variant<A, B, C> — «значение одного из нескольких типов».
  • Рекомендации по использованию

  • Давайте variant осмысленное имя через using: например, using ConfigValue = std::variant<int, std::string, bool>;.
  • Для простого чтения используйте std::get_if — он безопасен и понятен.
  • Для общей обработки всех типов изучите std::visit.
  • Не используйте variant как способ спрятать плохую модель данных.
  • Если вы хотите уверенно разобраться не только с std::variant, но и с современным C++ в целом — от базового синтаксиса до продвинутых приёмов, обратите внимание на курс «Программирование на C++ с Нуля до Гуру». Он хорошо подойдёт тем, кто хочет учиться системно, а не собирать знания по разрозненным статьям.

    Вывод

    std::variant в C++17 — практичный инструмент для ситуаций, когда значение может иметь один из нескольких известных типов. Он безопаснее union, выразительнее набора флагов и часто проще и понятнее наследования. Начните с std::get_if и std::holds_alternative, а затем переходите к std::visit — так вы быстро научитесь применять std::variant в реальных программах.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий