본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : std::variant (17)

by snowoods 2025. 9. 29.

Modern C++

 

std::variant

 

개요

std::variant는 타입 안전(type-safe) 공용체(union)로, C++17 표준 라이브러리에 추가된 기능입니다. 주어진 여러 타입 중 하나의 값을 가질 수 있으며, 현재 어떤 타입의 값을 저장하고 있는지 항상 기억합니다. 기존 C 스타일 공용체와 달리 타입 정보가 없어 발생할 수 있는 오류를 컴파일 또는 런타임에 방지할 수 있습니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++17: std::variant가 처음 도입되었습니다.

 

내용 설명

std::variant는 템플릿 인자로 명시된 타입들 중 하나의 인스턴스만 저장할 수 있는 컨테이너입니다. 예를 들어 std::variant<int, double, std::string>int, double, std::string 중 하나의 값만 가질 수 있습니다.

주요 기능은 다음과 같습니다.

  • 값 접근
    • std::get<T>(variant): 타입 T로 값에 접근합니다. 만약 variantT 타입의 값을 가지고 있지 않으면 std::bad_variant_access 예외를 던집니다.
    • std::get<I>(variant): 인덱스 I로 값에 접근합니다. 타입과 마찬가지로 현재 저장된 값의 인덱스가 I가 아니면 예외가 발생합니다.
  • 타입 확인
    • std::holds_alternative<T>(variant): variant가 현재 T 타입의 값을 가지고 있는지 확인하고 bool 값을 반환합니다.
  • 안전한 값 접근
    • std::get_if<T>(&variant): variantT 타입의 값을 가지고 있다면 해당 값에 대한 포인터를, 그렇지 않다면 nullptr를 반환합니다. 예외를 발생시키지 않아 안전합니다.
  • 방문자 패턴
    • std::visit(visitor, variant): variant가 현재 담고 있는 값에 대해 visitor 함수(주로 람다)를 호출합니다. 모든 타입을 안전하고 효율적으로 처리할 수 있는 가장 강력한 기능입니다.

 

예제 코드

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

int main()
{
    // int 타입으로 초기화
    std::variant<int, double, std::string> v = 42;

    // 1. std::get<T>로 값 접근 (성공)
    std::cout << "1. Initial value: " << std::get<int>(v) << '\n';

    // 2. std::get<T>로 값 접근 (실패 시 예외 발생)
    try
    {
        std::get<double>(v); // 현재 int를 담고 있으므로 예외 발생
    }
    catch (const std::bad_variant_access& e)
    {
        std::cout << "2. Exception caught: " << e.what() << '\n';
    }

    // 3. std::holds_alternative로 타입 확인 후 접근
    v = 42.0;
    if (std::holds_alternative<double>(v))
    {
        std::cout << "3. Value is double: " << std::get<double>(v) << '\n';
    }

    // 4. std::get_if로 안전하게 값 접근
    v = "Hello, variant!";
    if (auto p_str = std::get_if<std::string>(&v))
    {
        std::cout << "4. Value is string: " << *p_str << '\n';
    }
    else
    {
        std::cout << "4. Value is not a string.\n";
    }

    // 5. std::visit로 모든 타입 처리
    v = 100;
    std::cout << "5. Visiting variant: ";
    std::visit([](auto&& arg) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, int>)
            std::cout << "It's an int with value " << arg << '\n';
        else if constexpr (std::is_same_v<T, double>)
            std::cout << "It's a double with value " << arg << '\n';
        else if constexpr (std::is_same_v<T, std::string>)
            std::cout << "It's a string with value \"" << arg << "\"\n";
    }, v);

    return 0;
}

 

실행 결과

1. Initial value: 42
2. Exception caught: Unexpected index
3. Value is double: 42
4. Value is string: Hello, variant!
5. Visiting variant: It's an int with value 100

 

활용팁

  • 오류 처리: std::get을 사용할 때는 try-catch 블록으로 예외를 처리하거나, std::holds_alternative로 타입을 먼저 확인하는 것이 안전합니다. 하지만 가장 권장되는 방법은 예외를 발생시키지 않는 std::get_ifstd::visit를 사용하는 것입니다.
  • 상태 표현: 상태 머신(State Machine)에서 각 상태가 서로 다른 데이터를 가질 때 std::variant를 사용하면 상태와 데이터를 하나의 객체로 깔끔하게 관리할 수 있습니다.
  • std::visit 활용: std::visitvariant를 다루는 가장 일반적이고 강력한 방법입니다. 오버로딩된 람다 세트나 함수 객체를 전달하여 variant가 담고 있는 모든 타입에 대한 처리를 간결하게 작성할 수 있습니다.
  • 빈 상태: variant가 비어있는 상태를 표현하고 싶다면 첫 번째 템플릿 인자로 std::monostate를 추가할 수 있습니다. std::variant<std::monostate, int, std::string>와 같이 사용하면 기본 생성 시 monostate 상태가 됩니다.