C++17 표준 라이브러리의 std::variant 소개

Twitter icon류광, 2017-07-15 18:07
C++17 표준 라이브러리에 추가된 std::variant를 소개합니다.

저번 글에 이어 "핵심 C++ 표준 라이브러리" 부록을 방출합니다. 이번에는 std::variant입니다. 부록에 실린 것과 본질적으로 같은 내용이지만, 링크와 주석이 하나 추가되었습니다.

형식에 안전한 공용체, std::variant

std::any가 형식에 안전한 void*라면, std::variant는 형식에 안전한 공용체(union)라 할 수 있다. 필요한 헤더는 <variant>이다. 다음은 variant의 형식 안전성을 보여주는 예이다.

void f()
{
    variant<int, double, string> v = 123;

    cout << v << endl; // 123

    cout << std::get<int>(v) << endl; // 명시적으로 int를 요청.
                                      // 앞의 행과 같은 결과를 낸다.

    cout <<std::get<0>(v) << endl;   // 첫 번째 형식(int)의 값을 요청. 
                                      // 역시 같은 결과를 낸다.

    auto f = std::get<float>(v);      // (1) 컴파일 오류

    auto cond = std::get<string>(v);  // (2) 실행 시점 예외 발생
}

이 예제에서 vintdouble, bool을 담을 수 있는 variant 객체인데, (1)은 엉뚱하게 float 값을 요구했다. 이런 오류는 컴파일러가 잡아낼 수 있으므로 컴파일 오류가 발생한다. (2)는 v가 가질 수 있는 세 형식 중 하나인 string을 요구했으므로 컴파일 오류는 발생하지 않지만, 대신 실행 시점에서 std::bad_variant_access라는 예외가 발생한다. 이는 v가 현재 가지고 있는 값, 즉 마지막으로 설정된 값의 형식(int)이 요청된 형식(string)과 일치하지 않기 때문이다. 둘 다 union을 사용할 때 프로그래머들이 흔히 하는 실수인데, variant는 이런 실수들을 컴파일 시점에서 방지하거나 실행 시점에서 보고해준다. 이런 측면 때문에 variant를 “형식에 안전한 공용체”라고 부른다. 또한, (1)과 같은 컴파일 시점 점검 능력은 std::any에 비한 장점이기도 하다.

앞의 예제의 (2)에서 보듯이 variant를 사용할 때에는 “이 variant 객체에 어떤 형식의 값을 담을 수 있는가?”도 중요하지만 “현재 이 variant 객체에 어떤 형식의 값이 담겨 있는가?”도 중요하다. 이와 관련해서 variant는 다음과 같은 멤버·비멤버 인터페이스들을 제공한다. 다음 예를 보자.

void g()
{
    variant<int, double, bool> v = 123;

    // (1)
    auto pv = std::get_if<bool>(&v);           // pv는 nullptr

    // (2)
    auto cond = std::holds_alternative<int>(v) // cond는 true

    // (3)
    auto i = v.index();                        // i는 0
}
  1. std::get과는 달리 std::get_if는 포인터를 받으며, 형식 불일치 시 예외를 던지는 대신 널 포인터를 돌려준다.
  2. std::holds_alternative<T>(v)는 현재 v 객체의 형식이 T인지의 여부를 뜻하는 bool 값을 돌려준다.
  3. 멤버 변수 index는 현재 형식의 색인을 돌려준다. 지금 예에서 int가 0, double이 1, bool이 2이다. 이 색인은 std::get<i>(v) 형태로 활용할 수 있다. 공용체는 형식 안전성이 떨어질 뿐만 아니라, 소위 POD(plain old data) 형식들만 담을 수 있다. 반면 variant는 첫 예제(f(x))의 variant<int, double, string>에서 보듯이 POD가 아닌 본격적인 클래스도 담을 수 있다. 이 덕분에 성격이 크게 다른 형식들을 하나의 variant 객체로 공유할 수 있는데, 이러한 능력은 소위 ‘방문자 패턴(Visitor pattern)’의 구현에 도움이 된다. 실제로, 이를 위해 variant는 아예 std::visit이라는 비멤버 함수를 제공한다.[1]

  1. 이 부분은 variant의 활용에 중요하지만 지면 제한 때문에 간단하게만 언급했는데, 언제 std::variant와 방문자 패턴, 그리고 '꼬리표 달린 공용체(tagged union)'에 관해 보충 글을 써보겠습니다. 

태그: C++ C++17 표준 라이브러리

comments powered by Disqus