-----------------
title : 씹어먹는 C++ - <17 - 5. C++ 17 의 std::optional, variant, tuple 살펴보기>
cat_title : 17 - 5. C++ 17 의 std::optional, variant, tuple 살펴보기
tex_title : C++ 유틸리티 라이브러리 소개
path : /C++
publish_date : 2020-05-05
next_page: 310
-----------------

이번 강좌에서는

* `std::optional`
* `std::variant`
* `std::tuple`
  
에 대해서 알아봅니다.

![](/img/ChewingCpplogo.png)

안녕하세요 여러분! 이번 강좌에서는 기존 강좌에서 채 다루지 못한 C++ 표준 라이브러리에서 제공하는 여러가지 유용한 도구들에 대해 간단하게 설명하도록 하겠습니다.

참고로 아래에서 설명할 도구들에 대한 설명은 C++ 17 기준으로 작성되었으며, 그 이하 버전의 C++ 을 사용하는 경우 원하는 기능을 사용하지 못할 수 도 있습니다. 각각의 요소들에 대해서 최소 몇 이상의 C++ 을 사용해야 하는지 명시하였습니다.

### std::optional (C++ 17 이상 - <optional>)

예를 들어서 어떠한 `map` 에서 주어진 키에 대응하는 값이 있는지 확인하는 함수를 만들고 싶다고 해봅시다. 그렇다면 어떤 식으로 함수를 작성하면 될까요?

만약에 단순하게 짠다면 아래와 같이 작성할 수 있습니다.

```cpp-formatted
#include <iostream>
#include <map>
#include <string>

std::string GetValueFromMap(const std::map<int, std::string>& m, int key) {
  auto itr = m.find(key);
  if (itr != m.end()) {
    return itr->second;
  }

  return std::string();
}

int main() {
  std::map<int, std::string> data = {{1, "hi"}, {2, "hello"}, {3, "hiroo"}};
  std::cout << "맵에서 2 에 대응되는 값은? " << GetValueFromMap(data, 2)
            << std::endl;
  std::cout << "맵에서 4 에 대응되는 값은? " << GetValueFromMap(data, 4)
            << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
맵에서 2 에 대응되는 값은? hello
맵에서 4 에 대응되는 값은? 
```

와 같이 나옵니다.

잘 돌아가는 것 처럼 보입니다. 하지만 위 방식은 한 가지 문제점이 있습니다. 바로 맵에 주어진 키가 들어가 있지 않을 때 인데요;

```cpp-formatted
return std::string();
```

위 경우 빈 `string` 객체를 리턴하지만 만약에 진짜로 어떤 키에 대응하는 값이 빈 문자열이면 어떨까요? 따라서 위와 같은 방식은 **맵에 키가 존재하지 않는 경우** 와 **키가 존재하는데 대응하는 값이 빈 문자열인 경우** 를 제대로 구분하지 못하게 됩니다.

그렇다면 아래와 같은 방식은 어떨까요?

```cpp-formatted
#include <iostream>
#include <map>
#include <string>

std::pair<std::string, bool> GetValueFromMap(
  const std::map<int, std::string>& m, int key) {
  auto itr = m.find(key);
  if (itr != m.end()) {
    return std::make_pair(itr->second, true);
  }

  return std::make_pair(std::string(), false);
}

int main() {
  std::map<int, std::string> data = {{1, "hi"}, {2, "hello"}, {3, "hiroo"}};
  std::cout << "맵에서 2 에 대응되는 값은? " << GetValueFromMap(data, 2).first
            << std::endl;
  std::cout << "맵에 4 는 존재하나요 " << std::boolalpha
            << GetValueFromMap(data, 4).second << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
맵에서 2 에 대응되는 값은? hello
맵에 4 는 존재하나요 false
```

와 같이 나옵니다. 이번에는 아예 `std::pair` 를 이용해서 대응하는 값과 함께 실제 맵에 존재하는지의 유무를 같이 전달하도록 하였습니다. 이 방식도 꽤 괜찮아 보이지만 한 가지 문제점이 있습니다.

바로 맵에 키가 존재 하지 않을 때 디폴트 객체를 리턴해야 한다는 점입니다. 이는 몇 가지 문제점이 있는데;

1. 객체의 디폴트 생성자가 정의되어 있지 않을 수 도 있고
2. 객체를 디폴트 생성하는 것이 매우 오래 걸릴 수 도 있다

와 같기 때문입니다. 이와 같은 문제를 해결하기 위해서는 **원하는 값을 보관할 수 도, 안할 수 도 있는 클래스** 를 도입하는 것입니다.\sidenote{물론 다른 방식으로는 GetValueFromMap 에 bool& 를 받아서 객체의 존재 유무를 따로 뽑아낼 수 도 있습니다. 하지만 함수의 디자인 측면에서 그리 깔끔한 방식은 아니지요.} 이를 가능하게 한 것이 바로 `std::optional` 입니다.

`std::optional` 를 어떻게 사용하는지 아래 예제를 통해 간단히 살펴봅시다.

```cpp-formatted
#include <iostream>
#include <map>
#include <string>
#include <utility>

std::optional<std::string> GetValueFromMap(const std::map<int, std::string>& m,
                                           int key) {
  auto itr = m.find(key);
  if (itr != m.end()) {
    return itr->second;
  }

  // nullopt 는 <optional> 에 정의된 객체로 비어있는 optional 을 의미한다.
  return std::nullopt;
}

int main() {
  std::map<int, std::string> data = {{1, "hi"}, {2, "hello"}, {3, "hiroo"}};
  std::cout << "맵에서 2 에 대응되는 값은? " << GetValueFromMap(data, 2).value()
            << std::endl;
  std::cout << "맵에 4 는 존재하나요 " << std::boolalpha
            << GetValueFromMap(data, 4).has_value() << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
맵에서 2 에 대응되는 값은? hello
맵에 4 는 존재하나요 false
```

와 같이 잘 나옵니다.

먼저 `std::optional` 의 정의 부터 살펴봅시다.

```cpp-formatted
std::optional<std::string>
```

위와 같이 템플릿 인자로 `optional` 이 보관하고자 하는 객체의 타입을 써주시면 됩니다. 해당 `optional` 객체는 `std::string` 을 보관하던지, 아니면 안하던지 둘 중 하나의 상태만을 가지게 됩니다.

```cpp-formatted
auto itr = m.find(key);
if (itr != m.end()) {
  return itr->second;
}
```

그리고 `GetValueFromMap` 함수 안에서 키에 대응하는 값이 존재한다면 그냥 해당 값을 리턴하였습니다. `std::optional` 에는 보관하고자 하는 타입을 받는 생성자가 정의되어 있기 때문에 위와 같이 그냥 리턴하더라도 `optional` 객체로 알아서 만들어져서 리턴됩니다.

이 때 `optional` 의 가장 큰 장점으로, 객체를 보관하는 과정에서 **동적 할당이 발생하지 않는다** 라는 점입니다. 따라서 불필요한 오버헤드가 없습니다. 쉽게 생각해서, `optional` 자체에 객체가 포함되어 있다고 보시면 됩니다.

```cpp-formatted
// nullopt 는 <optional> 에 정의된 객체로 비어있는 optional 을 의미한다.
return std::nullopt;
```

만약에 아무런 객체도 가지고 있지 않은 빈 `optional` 객체를 리턴하고 싶다면, 그냥 `nullopt` 객체를 리턴하면 됩니다. `std::nullopt` 는 미리 정의되어 있는 빈 `optional` 객체를 나타냅니다.

```cpp-formatted
GetValueFromMap(data, 2).value()
```

만일 `optional` 객체가 가지고 있는 객체를 접근하고 싶다면 `value()` 함수를 호출하면 됩니다. 주의해야할 점은 만일 `optional` 이 가지고 있는 객체가 없다면 `std::bad_optional_access` 예외를 던지게 됩니다. 따라서 반드시 `optional` 가 들고 있는 객체에 접근하기 전에 실제로 값을 가지고 있는지 확인해야 하는데, 이는

```cpp-formatted
GetValueFromMap(data, 4).has_value()
```

처럼 `has_value` 함수로 사용하면 됩니다. 한 가지 유용한 팁으로 `optional` 객체 자체에 `bool` 로 변환하는 캐스팅 연산자가 포함되어 있으므로 그냥

```cpp-formatted
if (GetValueFromMap(data, 4))
```

나

```cpp-formatted
if (GetValueFromMap(data, 4).has_value())
```

는 동일한 의미의 문장이 되겠습니다. 마찬가지로 `value()` 함수 대신에 역참조 연산자를(`*`) 이용하셔도 됩니다. 즉 `GetValueFromMap(data, 2).value()` 와 `*GetValueFromMap(data, 2)` 는 동일한 문장 입니다.

이 `std::optional<T>` 가 `std::pair<bool, T>` 와 가장 큰 차이점이 바로 `pair` 와는 달리 **아무 것도 들고 있지 않는 상태에서 디폴트 객체를 가질 필요가 없다** 라는 점이였습니다. 해당 사실이 정말로 맞는지 아래 코드를 통해 확인해보겠습니다.

```cpp-formatted
#include <iostream>
#include <utility>

class A {
 public:
  A() { std::cout << "디폴트 생성" << std::endl; }

  A(const A& a) { std::cout << "복사 생성" << std::endl; }
};

int main() {
  A a;

  std::cout << "Optional 객체 만듦 ---- " << std::endl;
  std::optional<A> maybe_a;

  std::cout << "maybe_a 는 A 객체를 포함하고 있지 않기 때문에 디폴트 생성할 "
               "필요가 없다."
            << std::endl;
  maybe_a = a;
}
```

성공적으로 컴파일 하였다면

```exec
디폴트 생성
Optional 객체 만듦 ---- 
maybe_a 는 A 객체를 포함하고 있지 않기 때문에 디폴트 생성할 필요가 없다.
복사 생성
```

와 같이 나옵니다. 보시다시피 `optional` 객체에 `a` 객체를 전달하기 직전까지 디폴트 생성되었다는 메세지가 뜨지 않습니다 (처음에 `a` 만들 때 빼고). 정말로 `optional` 은 빈 객체 상태에서는 해당 객체를 가지고 있지 않는다는 사실을 알 수 있습니다.

이와 같이 `std::optional` 을 이용해서 **어떠한 객체를 보관하거나 말거나** 라는 의미를 쉽게 전달할 수 있습니다.

#### 레퍼런스를 가지는 std::optional

`std::optional` 의 한 가지 단점으로는 일반적인 방법으로는 레퍼런스를 포함할 수 없다는 점입니다. 예를 들어서 아래와 같이 레퍼런스에 대한 `optional` 객체를 정의하고 한다면

```cpp-formatted
#include <iostream>
#include <map>
#include <string>
#include <utility>

class A {
 public:
  A() { std::cout << "디폴트 생성" << std::endl; }

  A(const A& a) { std::cout << "복사 생성" << std::endl; }
};

int main() {
  A a;

  std::optional<A&> maybe_a = a;
}
```

컴파일 하였을 경우

```compiler-warning
/usr/include/c++/9/optional: In instantiation of ‘union std::_Optional_payload_base<A&>::_Storage<A&, true>’:
/usr/include/c++/9/optional:239:30:   required from ‘struct std::_Optional_payload_base<A&>’
/usr/include/c++/9/optional:295:12:   required from ‘struct std::_Optional_payload<A&, true, true, true>’
/usr/include/c++/9/optional:628:30:   required from ‘struct std::_Optional_base<A&, true, true>’
/usr/include/c++/9/optional:656:11:   required from ‘class std::optional<A&>’
test.cc:16:21:   required from here
/usr/include/c++/9/optional:212:15: error: non-static data member ‘std::_Optional_payload_base<A&>::_Storage<A&, true>::_M_value’ in a union may not have reference type ‘A&’
  212 |           _Up _M_value;
      |               ^~~~~~~~
/usr/include/c++/9/optional: In instantiation of ‘class std::optional<A&>’:
test.cc:16:21:   required from here
/usr/include/c++/9/optional:672:21: error: static assertion failed
  672 |       static_assert(!is_reference_v<_Tp>);
      |                     ^~~~~~~~~~~~~~~~~~~~
/usr/include/c++/9/optional:888:7: error: forming pointer to reference type ‘A&’
  888 |       operator->() const
      |       ^~~~~~~~
/usr/include/c++/9/optional:893:7: error: forming pointer to reference type ‘A&’
  893 |       operator->()
      |       ^~~~~~~~
```

와 같은 끔찍한 컴파일 오류를 보실 수 있습니다.

물론, 그렇다고 해서 레퍼런스를 `optional` 이 포함할 수 없는 것은 아닙니다. 바로 `std::reference_wrapper` 를 사용해서 레퍼런스 처럼 동작하는 `wrapper` 객체를 정의하면 됩니다.

```cpp-formatted
#include <functional>
#include <iostream>
#include <optional>
#include <utility>

class A {
 public:
  int data;
};

int main() {
  A a;
  a.data = 5;

  // maybe_a 는 a 의 복사복이 아닌 a 객체 자체의 레퍼런스를 보관하게 된다.
  std::optional<std::reference_wrapper<A>> maybe_a = std::ref(a);

  maybe_a->get().data = 3;

  // 실제로 a 객체의 data 가 바뀐 것을 알 수 있다.
  std::cout << "a.data : " << a.data << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
a.data : 3
```

와 같이 잘 실행되었음을 알 수 있습니다.

```cpp-formatted
std::optional<std::reference_wrapper<A>> maybe_a = std::ref(a);
```

`std::reference_wrapper` 는 레퍼런스가 아니라 일반적인 객체이기 때문에 `optional` 에 전달할 수 있습니다. `reference_wrapper` 를 `get()` 함수를 통해서 레퍼런스 하고 있는 객체를 얻어오게 됩니다.대신 `reference_wrapper` 객체를 생성하기 위해서는 `std::ref` 함수를 사용해야 합니다.

```cpp-formatted
maybe_a->get().data = 3;

// 실제로 a 객체의 data 가 바뀐 것을 알 수 있다.
std::cout << "a.data : " << a.data << std::endl;
```

앞서 `optional` 에 역참조 연산자가 정의되어 있어서 가지고 있는 값을 `*` 를 통해 간단하게 얻어낼 수 있다고 하였는데요, 이와 같은 선상에서 `->` 연산자 역시 정의되어 있어서 가지고 있는 값에 함수를 호출할 수 있습니다.

따라서 위와 같이 `reference_wrapper` 가 가지고 있는 `a` 에 대한 레퍼런스를 `get` 함수를 통해 얻어낼 수 있습니다. 그리고 해당 레퍼런스의 `data` 를 바꾸면 실제로 `a` 의 `data` 가 바뀐 것을 확인할 수 있죠!

이와 같이 `std::optional` 은 여러 모로 아주 쓸모가 많은 녀석 입니다. 현업에서 여러 모로 자주 활용할 수 있을 것이라 생각합니다.

### std::variant (C++ 17 이상 - <variant>)

`std::variant` 는 *one-of* 를 구현한 클래스라고 보시면 됩니다. 즉 컴파일 타임에 정해진 여러가지 타입들 중에 한 가지 타입의 객체를 보관할 수 있는 클래스 입니다.

물론 공용체(union) 을 이용해서 해결할 수 도 있겠지만, 공용체가 현재 어떤 타입의 객체를 보관하고 있는지 알 수 없기 때문에 실제로 사용하기에는 매우 위험합니다.

예를 들어서 아래 간단한 예제를 살펴봅시다.

```cpp-formatted
// 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` 의 가장 큰 특징으로는 반드시 값을 들고 있어야 한다는 점입니다. 만약에 그냥

```cpp-formatted
std::variant<int, std::string, double> v;
```

을 정의한다면 `v` 에는 첫 번째 타입 인자 (int) 의 디폴트 생성자가 호출되게 됩니다. 즉 위 경우 `v` 에는 0 이 들어가겠지요. 즉 *비어 있는* `variant` 는 불가능한 상태라고 보시면 됩니다.

`variant` 는 `optional` 과 비슷하게 객체의 대입 시에 어떠한 동적 할당도 발생하지 않습니다. 따라서 굉장히 작은 오버헤드로 객체들을 보관할 수 있습니다. 다만 `variant` 객체 자체의 크기는 나열된 가능한 타입들 중 가장 큰 타입의 크기를 따라갑니다.

`variant` 는 **이러이러한 타입들 중 하나(one-of)** 를 표현하기에 매우 적합한 도구 입니다. 예를 들어서 어떤 데이터 베이스에 검색을 해서 결과를 돌려주는 함수를 생각해봅시다. 이 결과는 조건에 따라 클래스 `A` 객체나 클래스 `B` 객체가 될 수 있습니다.

```cpp-formatted
class A {};
class B {};

/* ?? */ GetDataFromDB(bool is_a) {
  if (is_a) {
    return A();
  }
  return B();
}
```

여러분이라면 위 함수를 어떻게 만들 수 있을까요? 상황에서 따라서 `A` 나 `B` 객체를 리턴할 수 있는 함수를요.

한 가지 방법이라면 C++ 의 다형성(polymorphism)을 이용하는 것입니다. 이를 위해서는 `A` 와 `B` 클래스의 공통 부모가 정의되어 있어야 합니다.

```cpp-formatted
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` 를 이용하면 매우 간단하게 해결할 수 있습니다.

```cpp-formatted
#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()
}
```

성공적으로 컴파일 하였다면

```exec
0
I am A
```

와 같이 나옵니다.

```cpp-formatted
std::variant<A, B> GetDataFromDB(bool is_a) {
  if (is_a) {
    return A();
  }
  return B();
}
```

`variant` 역시 `optional` 과 마찬가지로 각각의 타입의 객체를 받는 생성자가 정의되어 있기 때문에 그냥 `A` 를 리턴하면 `A` 를 가지는 `variant` 가, `B` 를 리턴하면 `B` 를 가지는 `variant` 가 생성됩니다.

자 그렇다면 이렇게 `variant` 에서 원하는 값을 어떻게 뺄 수 있는지 살펴보도록 하겠습니다.

```cpp-formatted
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` 를 정의할 때 같은 타입을 여러 번 써주면 안됩니다. 예를 들어서

```cpp-formatted
std::variant<std::string, std::string> v;
```

는 컴파일 시 오류가 발생하게 됩니다.

#### std::monostate

만약에 굳이 `variant` 에 **아무 것도 들고 있지 않은 상태**를 표현하고자 싶다면 해당 타입으로 `std::monostate` 를 사용하면 됩니다. 이를 통해서 마치 `std::optional` 과 같은 효과를 낼 수 있습니다.

예를 들어서

```cpp-formatted
std::variant<std::monostate, A, B> v;
```

와 같이 `variant` 를 정의한다면 `v` 에는 아무것도 안들어 있거나 `A` 혹은 `B` 가 들어가 있을 수 있습니다. 또한 `variant` 안에 정의된 타입들 중에 디폴트 생성자가 있는 타입이 하나도 없는 경우 역시 `std::monostate` 를 활용하면 됩니다. 예를 들어서

```cpp-formatted
class A {
 public:
  A(int i) {}
};

class B {
 public:
  B(int i) {}
};
```

위와 같이 디폴트 생성자가 없는 클래스가 있다고 하였을 때;

```cpp-formatted
std::variant<A, B> v;
```

를 덩그러니 정의하게 되면 컴파일 오류가 발생하게 됩니다. 왜냐하면 앞서 이야기 하였듯이 `variant` 는 반드시 객체를 들고 있어야 하는데, 이를 지정하지 않을 경우 자동으로 첫 번째 타입의 디폴트 생성된 객체를 갖고 있으려고 하기 때문이죠. 하지만 위의 경우 A 의 디폴트 생성자가 없기 때문에 컴파일 오류가 발생합니다.

이 경우 그냥 첫 번째 타입으로 `std::monostate` 를 지정해주면 깔끔하게 해결됩니다.

```cpp-formatted
std::variant<std::monostate, A, B> v;
```

와 같이 하게 되면 디폴트로 `std::monostate` 가 v 에 들어가게 되어서 문제 없습니다.

### std::tuple (C++ 11 이상 - <tuple>)

마지막으로 여러 서로 다른 타입들의 묶음을 간단하게 다룰 수 있도록 제공하는 `std::tuple` 에 대해 살펴보겠습니다. C++ 에서 같은 타입 객체들을 여러개 다루기 위해서는 `std::vector` 나 배열을 사용하였습니다.

하지만 다른 타입의 객체들을 여러 개 다루는 방법은 꽤나 골치 아픕니다. 보통은 아래와 같이

```cpp-formatted
struct Collection {
  int a;
  std::string s;
  double d;
};
```

간단히 구조체를 정의해서 전달하곤 합니다. 하지만 매번 이렇게 의미 없는 구조체를 생성하게 된다면 코드를 읽는 사람 입장에서 상당히 골치아픕니다. 파이썬과 같은 언어에서는 `(1, 'abc', 3.14)` 처럼 간단히 `tuple` 을 생성할 수 있는데 말이죠.

다행이도 `C++ 11` 부터 `std::tuple` 라이브러리가 추가되어서 간단히 서로 다른 타입들의 집합을 생성할 수 있습니다.

```cpp-formatted
#include <iostream>
#include <string>
#include <tuple>

int main() {
  std::tuple<int, double, std::string> tp;
  tp = std::make_tuple(1, 3.14, "hi");

  std::cout << std::get<0>(tp) << ", " << std::get<1>(tp) << ", "
            << std::get<2>(tp) << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
1, 3.14, hi
```

와 같이 잘 나옵니다.

```cpp-formatted
std::tuple<int, double, std::string> tp;
```

`tuple` 을 정의하는 방법은 간단합니다. `tuple` 이 보관하고자 하는 타입들을 쭈르륵 나열해주면 됩니다. 위 `tp` 의 경우 `int, double, std::string` 이 세 개 타입의 객체를 보관하는 컨테이너라 생각하시면 됩니다.

참고로 `variant` 와는 다르게 `tuple` 에는 같은 타입들이 들어 있어도 전혀 문제가 될 것이 없습니다.

```cpp-formatted
tp = std::make_tuple(1, 3.14, "hi");
```

`tuple` 객체를 생성하기 위해서는 `make_tuple` 함수를 사용하면 됩니다.

```cpp-formatted
std::cout << std::get<0>(tp) << ", " << std::get<1>(tp) << ", "
          << std::get<2>(tp) << std::endl;
```

그리고 마지막으로 `tuple` 의 각각의 원소에 접근하기 위해서는 이전의 `variant` 처럼 `std::get` 을 이용하시면 됩니다. 이 때 `get` 에 템플릿 인자로 몇 번째 원소에 접근할지 지정해주면 됩니다.

참고로 원하는 타입의 원소를 뽑아내고 싶다면 타입을 전달해도 되는데, 예를 들어서 `std::get<std::string>` 을 하게 되면 `tuple` 에 정의된 문자열 객체가 뽑혀져 나오게 됩니다. 다만, `tuple` 에 `std::string` 이 없거나, 2 개 이상 존재한다면 예외가 발생하게 됩니다.

#### Structured binding (C++ 17 이상)

C++ 17 에서는 structured binding 이라는 테크닉이 추가되어서 `tuple` 을 좀더 편리하게 다룰 수 있게 되었습니다. 예를 들어서 아래와 같은 상황을 생각해봅시다.

```cpp-formatted
#include <iostream>
#include <string>
#include <tuple>

std::tuple<int, std::string, bool> GetStudent(int id) {
  if (id == 0) {
    return std::make_tuple(30, "철수", true);
  } else {
    return std::make_tuple(28, "영희", false);
  }
}

int main() {
  auto student = GetStudent(1);

  int age = std::get<0>(student);
  std::string name = std::get<1>(student);
  bool is_male = std::get<2>(student);

  std::cout << "이름 : " << name << std::endl;
  std::cout << "나이 : " << age << std::endl;
  std::cout << "남자 ? " << std::boolalpha << is_male << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
이름 : 영희
나이 : 28
남자 ? false
```

와 같이 잘 나옵니다. `tuple` 에서 각각의 원소들을 뽑아내기 위해서는 아래와 같이 해야 합니다.

```cpp-formatted
int age = std::get<0>(student);
std::string name = std::get<1>(student);
bool is_male = std::get<2>(student);
```

상당히 귀찮죠. 하지만 C++ 17 부터는 *structured binding* 이라는 방식을 통해 아주 간단하게 표현 할 수 있습니다.

```cpp-formatted
#include <iostream>
#include <string>
#include <tuple>

std::tuple<int, std::string, bool> GetStudent(int id) {
  if (id == 0) {
    return std::make_tuple(30, "철수", true);
  } else {
    return std::make_tuple(28, "영희", false);
  }
}

int main() {
  auto student = GetStudent(1);

  auto [age, name, is_male] = student;

  std::cout << "이름 : " << name << std::endl;
  std::cout << "나이 : " << age << std::endl;
  std::cout << "남자 ? " << std::boolalpha << is_male << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
이름 : 영희
나이 : 28
남자 ? false
```

와 같이 잘 나옵니다. structured binding 이 적용된 부분은 아래와 같습니다.

```cpp-formatted
auto [age, name, is_male] = student;
```

마치 파이썬을 생각나게 하는 문법 입니다. structured binding 을 사용하기 위해선

```cpp-formatted
auto /* & 혹은 && 도 가능 */ [/* tuple 안에 원소들을 받기 위한 객체*/] = tp;
```

와 같이 쓰면 됩니다. 자세한 내용은 [여기](https://en.cppreference.com/w/cpp/language/structured_binding) 에서 볼 수 있습니다.

예를 들어서 만약에 `tuple` 안에 객체들을 복사하지 않고 그냥 레퍼런스만 취하고 싶다면

```cpp-formatted
auto& [age, name, is_male] = student;
```

와 같이 하면 됩니다.

한 가지 중요한 점은 `tuple` 의 **모든** 원소들을 반드시 받아야 한다는 점입니다. 안타깝게도 structured binding 을 사용해선 중간의 원소 하나만 빼고 받기와 같은 것은 할 수 없습니다.

그래도 structured binding 은 여러가지 쓰임새들이 매우 많습니다. 꼭 `tuple` 말고도, 데이터 멤버들이 정의되어 있는 구조체의 데이터 필드들을 받는데에도 사용할 수 있습니다. 예를 들어서

```cpp-formatted
struct Data {
  int i;
  std::string s;
  bool b;
};

Data d;
auto [i, s, b] = d;
```

와 같이 각각의 데이터 필드를 받아낼 수 있습니다.

덕분에 `pair` 와 같은 클래스들 역시 structured binding 을 사용할 수 있습니다.

```cpp-formatted
#include <iostream>
#include <map>
#include <string>

int main() {
  std::map<int, std::string> m = {{3, "hi"}, {5, "hello"}};
  for (auto& [key, val] : m) {
    std::cout << "Key : " << key << " value : " << val << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
Key : 3 value : hi
Key : 5 value : hello
```

와 같이 나옵니다. 기존에는 `iterator` 로 받아서 `first` 와 `second` 로 키와 대응되는 값을 나타내야 하지만 `strucuted binding` 을 사용해서 훨씬 깔끔하게 나타낼 수 있습니다.

자 그렇다면 이것으로 이번 강좌를 마치도록 하겠습니다. C++ 에서 제공하는 유용한 도구들을 사용해서 코드를 훨씬 깔끔하게 나타내보세요!

### 마무리

아무래도 이번 강좌가 제 마지막 C++ 강좌가 될 것 같습니다. 다음 글에서는 C++ 강의를 긴 시간 동안 작성해오면서 느낀 짧은 소회를 적어보자 합니다. (물론 그렇다고 해서 아예 끝난 것은 아닙니다. C++ 은 정말 빠르게 발전하고 있기 때문에 C++ 20 이 본격적으로 도입이 된다면 또 이야기할 주제들이 생각나겠죠!)

감사합니다.

```lec-summary
`optional` 을 통해 원하는 데이터를 가지거나 가지지 않는 객체를 만들 수 있습니다.
`variant` 를 통해 *여러 타입 들 중 하나* 를 나타내는 객체를 만들 수 있습니다.
`tuple` 을 통해 *여러 서로다른 타입들의 모음* 을 나타내는 객체를 만들 수 있습니다.
```

##@ chewing-cpp-end