std::variant 는 one-of 를 구현한 클래스라고 보면 된다. 즉, 컴파일 타임에 정해진 여러가지 타입들 중에 한 가지 타입의 객체를 보관할 수 있는 클래스이다. 물론 공용체(union) 을 이용해서 해결할 수 도 있겠지만, 공용체가 현재 어떤 타입의 객체를 보관하고 있는지 알 수 없기 때문에 실제로 사용하기에는 매우 위험하다.
// v 는 이제 int
std::variant<int, std::string, double> v = 1;
// v 는 이제 std::string
v = "abc";
// v는 이제 double
v = 3.14;
먼저 variant 를 정의할 때 포함하고자 하는 타입들을 명시해줘야 한다. 위의 경우 정의한 variant 는 int, std::string, double 이 셋 중 하나의 타입을 가질 수 있다. variant 의 가장 큰 특징으로는 반드시 값을 들고 있어야 한다는 점이다.
std::variant<int, std::string, double> v;
그냥 정의한다면 v 에는 첫 번째 타입 인자 (int) 의 디폴트 생성자가 호출되게 된다. 즉 위 경우 v 에는 0 이 들어가는데, 비어 있는 variant 는 불가능한 상태라고 보면 된다.
variant 는 optional 과 비슷하게 객체의 대입 시에 어떠한 동적 할당도 발생하지 않는다. 따라서 굉장히 작은 오버헤드로 객체들을 보관할 수 있다. 다만 variant 객체 자체의 크기는 나열된 가능한 타입들 중 가장 큰 타입의 크기를 따라간다.
variant 는 이러이러한 타입들 중 하나(one-of) 를 표현하기에 매우 적합한 도구이다. 예를 들어서 어떤 데이터 베이스에 검색을 해서 결과를 돌려주는 함수를 생각해보자. 이 결과는 조건에 따라 클래스 A 객체나 클래스 B 객체가 될 수 있다.
class A {};
class B {};
/* ?? */ GetDataFromDB(bool is_a) {
if (is_a) {
return A();
}
return B();
}
한 가지 방법이라면 C++ 의 다형성(polymorphism)을 이용하는 것이다. 이를 위해서는 A 와 B 클래스의 공통 부모가 정의되어 있어야 한다.
class Data {};
class A : public Data {};
class B : public Data {};
std::unique_ptr<Data> GetDataFromDB(bool is_a) {
if (is_a) {
return std::make_unique<A>();
}
return std::make_unique<B>();
}
따라서 위와 같이 A 혹은 B 객체를 리턴할 수 있다. 그리고 해당 함수를 호출하는 곳에서 리턴하는 Data 의 실제 객체가 무엇인지 간단하게 알아낼 수도 있다.
하지만 위 문제는 리턴하고자 하는 클래스들의 부모 클래스가 공통으로 정의되어 있어야 하고, std::string 이나 int 와 같은 표준 클래스의 객체들에는 적용할 수 없다는 문제가 있다. 하지만 std::variant 를 이용하면 매우 간단하게 해결할 수 있다.
#include <iostream>
#include <memory>
#include <variant>
class A {
public:
void a() { std::cout << "I am A" << std::endl; }
};
class B {
public:
void b() { std::cout << "I am B" << std::endl; }
};
std::variant<A, B> GetDataFromDB(bool is_a) {
if (is_a) {
return A();
}
return B();
}
int main() {
auto v = GetDataFromDB(true);
std::cout << v.index() << std::endl;
std::get<A>(v).a(); // 혹은 std::get<0>(v).a()
}
// 실행 결과
// 0
// I am A
variant 역시 optional 과 마찬가지로 각각의 타입의 객체를 받는 생성자가 정의되어 있기 때문에 그냥 A 를 리턴하면 A 를 가지는 variant 가, B 를 리턴하면 B 를 가지는 variant 가 생성된다.
std::cout << v.index() << std::endl;
std::get<A>(v).a(); // 혹은 std::get<0>(v).a()
먼저 현재 variant 에 몇 번째 타입이 들어있는지 알고 싶다면 index() 함수를 사용하면 된다. 위의 경우 A 타입의 객체가 들어 있는데 A 는 variant 에서 첫 번째 타입이므로 0 을 리턴하게 된다.
그 다음으로 실제로 원하는 값을 뽑아내고 싶다면 외부에 정의되어 있는 함수인 std::get<T> 를 이용하면 된다. 이 때 이 T 자리에 뽑아내고자 하는 타입을 써주던지, 아니면 해당 타입의 index 를 넣어주면 된다.
따라서 A 를 뽑고 싶다면 std::get<A>(v) 나 std::get<0>(v) 를 하면 되고, B 를 뽑고 싶다면 std::get<B>(v) 나 std::get<1>(v) 를 하면 된다.
여기서 한 가지 알 수 있는 점은 varinat 가 보관하는 객체들은 타입으로 구분된다는 점이다. 따라서 variant 를 정의할 때 같은 타입을 여러 번 써주면 안된다.
std::variant<std::string, std::string> v;
std::monostate
만약에 굳이 variant 에 아무 것도 들고 있지 않은 상태를 표현하고자 싶다면 해당 타입으로 std::monostate 를 사용하면 된다. 이를 통해서 마치 std::optional 과 같은 효과를 낼 수 있다.
std::variant<std::monostate, A, B> v;
위와 같이 variant 를 정의한다면 v 에는 아무것도 안들어 있거나 A 혹은 B 가 들어가 있을 수 있다. 또한 variant 안에 정의된 타입들 중에 디폴트 생성자가 있는 타입이 하나도 없는 경우 역시 std::monostate 를 활용하면 된다. 예를 들어서
class A {
public:
A(int i) {}
};
class B {
public:
B(int i) {}
};
std::variant<std::monostate, A, B> v;
출처 : C++ 기초 개념 17-5 : std::optional, variant, tuple (koreanfoodie.me)
'프로그래밍 언어 > C++' 카테고리의 다른 글
C++ 데이터 타입(data type) (0) | 2022.10.23 |
---|---|
C++ std::tuple 여러가지 타입들의 객체를 보관 / Structured binding (0) | 2022.10.12 |
C extern (변수 / 함수 외부 선언) (0) | 2022.09.27 |
C++ auto 타입 추론 (0) | 2022.09.21 |
C++ mutable (0) | 2022.09.15 |