개요
이전에 Union은 몇 가지 문제를 가지고 있었다.
- int를 써놓고 double을 읽는 행위 (undefined behavior)
- Object가 들어갔을 때 타입을 바꿀때마다 직접적으로 생성자와 소멸자를 호출해줘야한다.
이러한 문제들을 해결해 줄 수 있는 것이 C++ 17 부터 추가된 std::variant인데 훨씬 더 안전한 union을 사용할 수 있도록 만들어준다.
std::variant 예제
struct S // 4 +(4) + 8 + 4 + (4) = 24
{
int i; // 4
double d; // 8
float f; // 4
};
union U // 8
{
int i; // 4
double d; // 8
float f; // 4
};
std::variant<int, double, float> v;
std::cout << "S" << sizeof(S) << std::endl; // 24 byte
std::cout << "U" << sizeof(U) << std::endl; // 8 byte
std::cout << "V" << sizeof(v) << std::endl; // 16 byte

결과를 보면 v의 크기는 16 byte다. 이유는 std::variant 자체도 변수가 어떤 타입인지 추적을 해야하기 때문에 union처럼 하나의 메모리 공간을 같이 사용하고 있음에도 뒷부분에 데이터 타입이 따로 들어있다. 데이터 타입으로 8 byte가 추가되므로 총 16 byte가 되는 것이다.
Union vs std::variant
- 이전에 union에서는
U u;
u.i = 10;
std::cout << u.d << std::endl; // undefined behavior
int를 쓰고 double로 읽는 행위를 하게되면 undefined behavior로 예상치 못한 결과가 나온다.
- 똑같은 코드를 std::variant를 사용해보면
std::variant<int, double, float> v;
v = 10;
try {
std::cout << std::get<double>(v) << std::endl;
} catch (...) { }
Exception이 던져진다. try / catch 구문을 통해 얼마든지 핸들링이 가능하다는 의미이다.
try / catch가 싫다면 if문을 사용할 수 있다.
if (auto pVal = std::get_if<double>(&v))
{
std::cout << *pVal << std::endl;
}
else
{
std::cout << "v is not type double" << std::endl;
}
현재 v 가 double 타입인지 체크하고 value를 출력할 수 있다. 즉, std::variant를 사용하게 되면 현재 어떤 타입이 들어있는지 추적이 가능하기 때문에 안전한 코드를 만들 수 있게 도와준다.
또한, union에 오브젝트 타입이 들어가게되면 직접 생성자와 소멸자를 호출해야했다.
하지만 std::variant를 사용하게 되면,
std::variant<std::string, std::vector<int>> sv;
sv = std::string(L"abcd");
std::cout << std::get<std::string>(v) << std::endl;
sv = std::vector{ 1,2,3 };
직접적으로 생성자 또는 소멸자를 호출할 필요없이 vector를 넣어주면 자동적으로 생성자와 소멸자를 관리해주면서 문제없이 vector를 사용할 수 있게된다.
Error Code Return 사용
std::variant는 union을 대체하는 것뿐만 아니라 ErrorCode를 리턴해주는데 사용할 수도 있다.
// enum class ErrorCode
std::variant<int ,ErrorCode> divide(int a, int b)
{
if (b == 0)
{
return ErrorCode::divide0;
}
return a / b;
}
std::optional을 사용하는 방법도 있으나 std::optional은 타입 자체가 유효한지 아닌지만 리턴해줄 수 있기 때문에 더 많은 Error 정보를 리턴해줄 때는 std::tuple이나 std::pair를 사용하는 방법이 있었다. 이를 std::variant를 사용하게 되면 똑같은 메모리 공간을 int와 ErrorCode가 공유하면서 리턴해줄 수 있다.
int와 ErrorCode는 같은 공간을 공유해도 문제가 없다. divide함수가 유효하다면 a / b 결과인 int 타입을 넣어주면 되고 그렇지 않다면 에러코드를 넣어서 리턴해주면 되기 때문에 메모리는 공유하지만 std::tuple 처럼 사용할 수 있다.
std::variant가 메모리를 공유함에도 불구하고 std::pair나 std::tuple과 아무런 차이없이 사용할 수 있는 것이다.
결론
std::variant는 union이 훨씬 더 안전해졌지만 내부적으로 타입 추적이 들어가고 호출때마다 타입을 체크해주어야하기 때문에 어느정도 오버헤드가 있는 것은 사실이다. 개발환경에 따라 이 정도는 무시할만하다고 생각된다면 어찌되었건 std::variant을 사용하는 것이 좋다.
'C++ > Modern C++' 카테고리의 다른 글
[C++] Type Punning (타입 장난) (0) | 2022.11.10 |
---|---|
[C++] Union (0) | 2022.11.04 |
[C++17] std::optional (0) | 2022.10.31 |
[C++] 가상함수 원리 (0) | 2022.10.24 |
[C++] attributes(속성) (0) | 2022.10.13 |