[C++] std::variant : 타입 안전한 union

[C++] std::variant : 타입 안전한 union

안녕하세요. 이번 글에서는 C++17에서 추가된 std::variant에 대해 알아보겠습니다.

여러 타입 중 하나의 값만 담는 상황에서는 예전에는 union을 쓰거나 부모 클래스를 두고 상속을 이용했습니다. union은 현재 어떤 타입이 들어 있는지 직접 관리해야 하고 std::string 같은 클래스 타입을 다루기 까다롭습니다. variant는 타입 안전한 union으로, 현재 담긴 타입을 스스로 기억합니다.

기본 사용법

헤더는 <variant>이며 C++17 이상(-std=c++17)이 필요합니다. 템플릿 인자로 담을 수 있는 타입 목록을 나열하고, 값을 대입하면 해당 타입이 활성화됩니다. 기본 생성하면 첫 번째 타입의 기본값을 가집니다.

#include <iostream>
#include <string>
#include <variant>
using namespace std;

int main()
{
    variant<int, double, string> v;     // int 0 (첫 번째 타입)
    cout << v.index() << "\n";          // 0

    v = 3.14;
    cout << v.index() << "\n";          // 1

    v = string("hello");
    cout << v.index() << "\n";          // 2
    cout << get<string>(v) << "\n";     // hello
}

값 꺼내기 : get, get_if, holds_alternative

함수설명
index()현재 활성 타입의 번호 (0부터)
holds_alternative<T>(v)현재 T가 담겨 있는지 확인
get<T>(v) / get<N>(v)값 반환. 다른 타입이면 std::bad_variant_access 예외
get_if<T>(&v)T가 담겨 있으면 포인터, 아니면 nullptr (예외 없음)
variant<int, string> v = 42;

if (holds_alternative<int>(v))
    cout << "int : " << get<int>(v) << "\n";

if (auto p = get_if<string>(&v))
    cout << "string : " << *p << "\n";
else
    cout << "string이 아닙니다\n";

try
{
    cout << get<string>(v);             // 활성 타입이 int이므로 예외
}
catch (const bad_variant_access& e)
{
    cout << e.what() << "\n";
}

std::visit : 타입별로 처리하기

담긴 타입에 따라 다르게 동작해야 할 때는 std::visit을 씁니다. 모든 타입을 처리하는 호출 가능 객체를 넘기면 컴파일 타임에 빠진 타입이 없는지 검사됩니다. 람다 여러 개를 하나로 묶는 overloaded 도우미를 함께 쓰는 방식이 흔합니다.

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;   // C++17에서 필요

variant<int, double, string> v = string("variant");

visit(overloaded{
    [](int i)           { cout << "int : " << i << "\n"; },
    [](double d)        { cout << "double : " << d << "\n"; },
    [](const string& s) { cout << "string : " << s << "\n"; }
}, v);

주의할 점

  • 기본 생성이 불가능한 타입이 첫 번째에 오면 variant도 기본 생성할 수 없습니다. 이때는 첫 번째에 std::monostate를 두어 “값 없음” 상태를 표현합니다.
  • 같은 타입을 두 번 나열하면 타입으로 접근할 수 없으므로 get<N> 처럼 인덱스로 접근해야 합니다.
  • 대입 도중 예외가 발생하면 값이 없는 상태(valueless_by_exception)가 될 수 있습니다.
  • 값이 있는지 없는지만 표현하려면 std::optional이 더 단순합니다.

참고

이상으로 std::variant에 대해 간단히 알아보았습니다.