(class template) variant

std::variant

variant는 공용체의 진화된 버전으로, 타입 세이프한 공용체라고 말할 수 있다. union(공용체)는 들어있는 타입이 무엇인지 알 수 없지만, variant(변형)은 들어있는 타입을 유지해주기 때문에 타입에 안전한 공용체라는 의미로 type-safe union 혹은 safer union라고 불리기도 한다.

 

 

 

variant 특징

1) union처럼 행동하지만, 타입에 안전하다.

: 가장 큰 특징으로, 타입을 유지하는 것 외에는 union과 모두 동일하다. union은 타입을 정의할 때 사용할 여러개의 타입을 함께 작성해서 사용하지만, variant는 타입에 대한 선언과 정의를 동시에 하며 이때 여러 개의 타입을 함께 작성한다.

 

2) 템플릿 인수 타입 중 하나이어야 한다.

: variant 변수를 선언했다고 모든 타입으로 변형이 가능한 것은 아니다. variant 변수를 선언할 때 개발자는 해당 variant 변수 내에서 변형 가능한 타입들을 함께 작성해야 한다.

 

3) 방문자 패턴(visitor pattern)에 자주 사용된다.

: variant 타입은 방문자 패턴인 visit 함수과 함께 사용되는 경우가 정말 많다. variant가 방문자 패턴에서도 사용할 수 있도록 잘 설계되었기 때문에 쿵짝이 잘 맞는다. 참고로, 최신 스타일의 코딩방법이라 불리는 모던 C++에서도 소개된 방법이다.

※ C++17 부터 지원한다.

 

 

 

코드로 variant 이해하기

특징 1) union처럼 행동하지만, 타입에 안전하다.

아래 예제는 variant타입의 변수 vint타입의 값인 10을 넣은 후, double타입의 값은 3.14를 넣었다. 두 타입은 다르지만 기존 union타입처럼 variant타입의 변수에서도 저장이 가능하다. 그리고 여기서 한가지 추가 설명을 하자면, variant변수에 두번째 값인 3.14를 넣을 때, 기존에 넣었던 첫번째 값인 10을 파기해서 초기화 한 후 두번째 값을 넣는다는 것이다. 참고로, variant 에서 제공하는 함수 중 index()는 variant 내에 정의되어 있는 type의 위치 index를 반환한다. 따라서 실행 결과는 int는 0, double은 1의 위치가 출력된다. 

#include <iostream>
#include <utility>
#include <variant>

int main() {
  std::variant<int, double> v = 10;
  std::cout << v.index() << std::endl;
  
  v = 3.14;
  std::cout << v.index() << std::endl;
}

실행 결과 : 

0
1

 

특징 2) 템플릿 인수 타입 중 하나이어야 한다.

variant변수를 선언할 때 템플릿 안에 변형가능한 타입을 함께 작성해야 한다. 그리고 사용할 때는 작성한 타입들에 한해서만 값을 저장할 수 있다. 아래 예제에서는 variant변수가 intdouble타입의 값만 가질 수 있도록 했기 때문에, string처럼 다른 타입을 저장하려고 시도한다면 에러가 발생할 것이다.

#include <iostream>
#include <utility>
#include <variant>

int main() {
    std::variant<int, double> v;
    v = 123;
    v = "hello"; // error
}

실행 결과 :

에러 발생

 

특징 3) 방문자 패턴(visitor pattern)에 자주 사용된다.

variant(=std::variant) 타입은 visit(=std::visit)과 함께 자주 사용되기 때문에 알아두면 도움이 될 것이다. 아래는 방문자 패턴을 위해 type-matching visitor를 구현하는 몇가지 방법을 소개한 것이다. 세가지 방법을 예로 들었지만, 아래 그림처럼 모두 동일한 처리를 하는 것을 뿐 코딩 형태만 다른 것이다. variant변수에 들어갈 수 있는 타입이 여러 개가 가능한 구조이기 때문에, 각 타입에서 맞게 직접 처리할 수 있도록 구조를 분리시킨 방문자 패턴과 찰떡궁합인 것이다.

 

variant & visitor pattern

 

아래 소개된 예들을 참고해서 개발할 때 상황에 맞게 알맞은 스타일로 코딩을 하면된다. 참고로,아래 예시코드들에서 visitor들은 리턴을 하지 않고, 입력 인자를 1개로 사용한 것이다. 필요하다면 예시를 약간 수정해서 리턴을 하도록 변경하거나 입력 인자를 2, 3개 등으로 변경하는데 어렵지 않을 것이라 생각된다.

 

첫번째 방법 : 람다(lambda)식 표현을 사용해서 각 타입을 다르게 처리

#include <iostream>
#include <utility>
#include <variant>

struct Ironman {};
struct Batman {};
struct Spiderman {};

int main() {
    std::variant<Ironman, Batman, Spiderman> jk{Batman()};
    std::visit([](auto&& args) {
        using T = std::decay_t<decltype(args)>;
        if constexpr (std::is_same_v<T, Ironman>)
          std::cout << "Ironman visitor" << std::endl;
        else if constexpr (std::is_same_v<T, Batman>)
          std::cout << "Batman visitor" << std::endl;
        else if constexpr (std::is_same_v<T, Spiderman>)
          std::cout << "Spiderman visitor" << std::endl;
    }, jk);
}

 

두번째 방법 : operator를 overload하는 object를 사용

#include <iostream>
#include <utility>
#include <variant>

struct Ironman {};
struct Batman {};
struct Spiderman {};

struct Joker {
    void operator()(Ironman &obj) { std::cout << "Ironman's visitor" << std::endl; }
    void operator()(Batman &computer) { std::cout << "Batman's visitor" << std::endl; }
    void operator()(Spiderman &computer) { std::cout << "Spiderman's visitor" << std::endl; }
};

int main() {
    std::variant<Ironman, Batman, Spiderman> jk{Batman()};
    std::visit(Joker(), jk);
}

 

세번째 방법 : operator를 overload하는 templete을 사용

#include <iostream>
#include <utility>
#include <variant>

template <class ...Ts> struct overload : Ts...{using Ts::operator()...; };
template <class ...Ts> overload(Ts...)->overload<Ts...>;

struct Ironman {};
struct Batman {};
struct Spiderman {};

int main() {
    std::variant<Ironman, Batman, Spiderman> jk{Batman()};
    std::visit(overload{
      [](Ironman&) { std::cout << "Ironman's visitor" << std::endl; },
      [](Batman&) { std::cout << "Batman's visitor" << std::endl; },
      [](Spiderman&) { std::cout << "Spiderman's visitor" << std::endl; }
    }, jk);
}

 

실행결과 (첫번째, 두번째, 세번째 모두 동일)

Batman's visitor