모두의 코드
씹어먹는 C++ - <17 - 5. C++ 17 의 std::optional, variant, tuple 살펴보기>
이번 강좌에서는
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
를 이용해서 대응하는 값과 함께 실제 맵에 존재하는지의 유무를 같이 전달하도록 하였습니다. 이 방식도 꽤 괜찮아 보이지만 한 가지 문제점이 있습니다.
바로 맵에 키가 존재 하지 않을 때 디폴트 객체를 리턴해야 한다는 점입니다. 이는 몇 가지 문제점이 있는데;
객체의 디폴트 생성자가 정의되어 있지 않을 수 도 있고
객체를 디폴트 생성하는 것이 매우 오래 걸릴 수 도 있다
와 같기 때문입니다. 이와 같은 문제를 해결하기 위해서는 원하는 값을 보관할 수 도, 안할 수 도 있는 클래스 를 도입하는 것입니다. 이를 가능하게 한 것이 바로 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 는 <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; }
성공적으로 컴파일 하였다면
실행 결과
맵에서 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 는 <optional> 에 정의된 객체로 비어있는 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_wrapper
를 get()
함수를 통해서 레퍼런스 하고 있는 객체를 얻어오게 됩니다.대신 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
를 바꾸면 실제로 a
의 data
가 바뀐 것을 확인할 수 있죠!
이와 같이 std::optional
은 여러 모로 아주 쓸모가 많은 녀석 입니다. 현업에서 여러 모로 자주 활용할 수 있을 것이라 생각합니다.
std::variant (C++ 17 이상 - <variant>)
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(); }
여러분이라면 위 함수를 어떻게 만들 수 있을까요? 상황에서 따라서 A
나 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
와 같이 나옵니다.
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
타입의 객체가 들어 있는데 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<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
에 정의된 문자열 객체가 뽑혀져 나오게 됩니다. 다만, tuple
에 std::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
로 받아서 first
와 second
로 키와 대응되는 값을 나타내야 하지만 strucuted binding
을 사용해서 훨씬 깔끔하게 나타낼 수 있습니다.
자 그렇다면 이것으로 이번 강좌를 마치도록 하겠습니다. C++ 에서 제공하는 유용한 도구들을 사용해서 코드를 훨씬 깔끔하게 나타내보세요!
마무리
아무래도 이번 강좌가 제 마지막 C++ 강좌가 될 것 같습니다. 다음 글에서는 C++ 강의를 긴 시간 동안 작성해오면서 느낀 짧은 소회를 적어보자 합니다. (물론 그렇다고 해서 아예 끝난 것은 아닙니다. C++ 은 정말 빠르게 발전하고 있기 때문에 C++ 20 이 본격적으로 도입이 된다면 또 이야기할 주제들이 생각나겠죠!)
감사합니다.
뭘 배웠지?
optional
을 통해 원하는 데이터를 가지거나 가지지 않는 객체를 만들 수 있습니다.
variant
를 통해 여러 타입 들 중 하나 를 나타내는 객체를 만들 수 있습니다.
tuple
을 통해 여러 서로다른 타입들의 모음 을 나타내는 객체를 만들 수 있습니다.

댓글을 불러오는 중입니다..