모두의 코드
씹어먹는 C++ - <17 - 5. C++ 17 의 std::optional, variant, tuple 살펴보기>

작성일 : 2020-05-05 이 글은 2357 번 읽혔습니다.

이번 강좌에서는

  • std::optional

  • std::variant

  • std::tuple

에 대해서 알아봅니다.

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

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

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

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

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

#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;
}

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

실행 결과

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

와 같이 나옵니다.

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

return std::string();

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

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

#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;
}

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

실행 결과

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

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

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

  1. 객체의 디폴트 생성자가 정의되어 있지 않을 수 도 있고

  2. 객체를 디폴트 생성하는 것이 매우 오래 걸릴 수 도 있다

와 같기 때문입니다. 이와 같은 문제를 해결하기 위해서는 원하는 값을 보관할 수 도, 안할 수 도 있는 클래스 를 도입하는 것입니다.

이를 가능하게 한 것이 바로 std::optional 입니다.

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

#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 는 <utility> 에 정의된 객체로 비어있는 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;
}

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

실행 결과

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

와 같이 잘 나옵니다.

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

std::optional<std::string>

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

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

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

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

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

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

GetValueFromMap(data, 2).value()

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

GetValueFromMap(data, 4).has_value()

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

if (GetValueFromMap(data, 4))

if (GetValueFromMap(data, 4).has_value())

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

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

#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;
}

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

실행 결과

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

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

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

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

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

#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;
}

컴파일 하였을 경우

컴파일 오류

/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 객체를 정의하면 됩니다.

#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;
}

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

실행 결과

a.data : 3

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

std::optional<std::reference_wrapper<A>> maybe_a = std::ref(a);

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

maybe_a->get().data = 3;

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

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

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

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

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

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

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

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

// v 는 이제 int
std::variant<int, std::string, double> v = 1;

// v 는 이제 std::string
v = "abc";

// v는 이제 double
v = 3.14;

먼저 variant 를 정의할 때 포함하고자 하는 타입들을 명시해줘야 합니다. 우리의 경우 정의한 variantint, std::string, double 이 셋 중 하나의 타입을 가질 수 있습니다.

variant 의 가장 큰 특징으로는 반드시 값을 들고 있어야 한다는 점입니다. 만약에 그냥

std::variant<int, std::string, double> v;

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

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

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

class A {};
class B {};

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

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

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

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

와 같이 나옵니다.

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

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

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

std::cout << v.index() << std::endl;
std::get<A>(v).a();  // 혹은 std::get<0>(v).a()

먼저 현재 variant 에 몇 번째 타입이 들어있는지 알고 싶다면 index() 함수를 사용하면 됩니다. 우리의 경우 A 타입의 객체가 들어 있는데 Avariant 에서 첫 번째 타입이므로 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<A, B> v;

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

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

std::variant<std::monostate, A, B> v;

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

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

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

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

struct Collection {
  int a;
  std::string s;
  double d;
};

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

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

#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;
}

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

실행 결과

1, 3.14, hi

와 같이 잘 나옵니다.

std::tuple<int, double, std::string> tp;

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

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

tp = std::make_tuple(1, 3.14, "hi");

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

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 에 정의된 문자열 객체가 뽑혀져 나오게 됩니다. 다만, tuplestd::string 이 없거나, 2 개 이상 존재한다면 예외가 발생하게 됩니다.

Structured binding (C++ 17 이상)

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

#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;
}

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

실행 결과

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

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

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

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

#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;
}

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

실행 결과

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

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

auto [age, name, is_male] = student;

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

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

와 같이 쓰면 됩니다. 자세한 내용은 여기 에서 볼 수 있습니다.

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

auto& [age, name, is_male] = student;

와 같이 하면 됩니다.

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

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

struct Data {
  int i;
  std::string s;
  bool b;
};

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

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

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

#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;
  }
}

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

실행 결과

Key : 3 value : hi
Key : 5 value : hello

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

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

마무리

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

감사합니다.

뭘 배웠지?

optional 을 통해 원하는 데이터를 가지거나 가지지 않는 객체를 만들 수 있습니다.

variant 를 통해 여러 타입 들 중 하나 를 나타내는 객체를 만들 수 있습니다.

tuple 을 통해 여러 서로다른 타입들의 모음 을 나타내는 객체를 만들 수 있습니다.

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <17 - 5. C++ 17 의 std::optional, variant, tuple 살펴보기> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 2 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요