optional, variant, tuple

하루공부·2024년 1월 22일
0

C++

목록 보기
20/25
post-thumbnail

C++ 아이콘 제작자: Darius Dan - Flaticon


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;
	}
	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;
}
  • 실행하면 존재하지 않는 키 '4'에 대해서 그냥 아무것도 대응되는 값을 출력안 할 뿐 프로그램은 잘 작동한다.
  • 그러면 key에 대응하는 값이 빈 문자열 and 맵에 key가 존재하지 않는경우
    ==> 구별할 수 가 없다.
  • 이는 대응되는 값 말고 실제로 있는지 없는지 체크를 하는 값까지 같이 반환을 해서 해결할 수 있다.
  • 그럼 보통 std::pair<std::string, bool> 을 이용해 반환 할 텐데 존재하지 않는 값에 대해서는 대응되는 값의 디폴트 생성자를 만들어 반환한다.

    여기에 디폴트 생성자가 없을 수 있고 객체에 따라 디폴트 생성이 비효율(부담)이 될 수 있다.(크기가 큰 메모리 할당이라든지)

간단하게 값이 있고 없음을 표현할 수 있으며 있을 때만 값을 뽑을 수 있으면 좋겠는데....


  • C++17 표준 라이브러리에 추가된 optional로 간단하게 표현할 수 있다.
    ==> 쉽게 원하는 값을 보관하거나 or 아무것도 보관 안하거나 2가지 상태를 가진다.

  • std::optional<> 템플릿 인자로 보관하고 싶은 객체 타입을 전달
    그렇게 만들어진 optional 객체는 해당 타입 데이터를 갖던가 없던가 2개의 상태가 된다.

#include <iostream>
#include <map>
#include <string>
#include <optional>

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

optional에 보관하는 타입을 받는 생성자가 정의되어 있어서 그냥 리턴하면 optional 객체가 반환된다

이 때 가장 큰 장점은 객체를 보관하는 과정에 동적 할당이 발생하지 않는다.
따라서 효율적이다.

  • optional 객체에 들어있는 객체에 접근하고 싶으면
    멤버 함수인 value()를 사용하거나 역참조 사용
    그리고 -> 연산자로 데이터 멤버에 접근 가능하다
  • 값이 있는지 없는지 확인할 때는 멤버 함수인 has_value() 사용.
  • optional 객체 자체에 bool 캐스팅 연산자가 있어서 if문 같은 곳에 편하게 사용.
  • 만약 비어있는 optional 객체를 리턴하고 싶다면 std::nullopt 객체를 리턴하면 된다.
  • std::nullopt는 optional에 정의된 초기화되지 않은 상태를 나타내는 빈 클래스 형식이다.
  • 값이 없으면 빈 값말고 내가 설정한 값을 반환 할 수 있다.
  • 멤버함수 value_or() 사용인데 정의를 보면 optional에 들어간 타입으로만 default값으로 설정이 가능하고 값이 있다면 해당 값을 반환하고 값이 없으면 설정한 default값을 반환한다.

  • 그런데 레퍼런스를 가질 때는 std::reference_wrapper 를 사용해서 레퍼런스 처럼 동작하는 wrapper 객체를 정의해야 한다. ==> 레퍼런스가 아닌 그냥 객체이다.
  • 대신 reference_wrapper 객체 생성할 때는 std::ref 함수로 초기화를 해야한다.
std::optional<std::reference_wrapper<A>> r_a = std::ref(a);

get()을 사용하여 wrappinng된 객체를 가져온다.,



variant

  • 여러 타입들 중 상황에 따라 타입 1개를 표현할 때 적합한 도구이다.

  • 공용체와 비슷한 친구인데 보다 더 안전하고 유연한 방식이다.

  • 컴파일 타임에 정해진 여러 타입들 중 1가지 타입의 객체를 보관할 수 있는 클래스다.
    ==> 런타임에 어떤 타입인지 검사할 필요가 없다.

  • 먼저 variant 를 정의할 때 포함하고자 하는 타입들을 명시해야 한다

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

만약 초기화 없이 사용한다면 첫 번째 인자의 타입으로 디폭트 생성자가 호출되어 0 이 들어간다.

만약 생성하는 타입들이 디폴트 생성자가 없다면 컴파일 오류가 생긴다

  • variant 는 optional 과 비슷하게 객체의 대입 시에 어떠한 동적 할당도 발생하지 않는다.

    따라서 굉장히 작은 오버헤드로 객체들을 보관할 수 있다.
    다만 variant 객체 자체의 크기는 나열된 가능한 타입들 중 가장 큰 타입의 크기를 따라간다.


  • 그럼 variant에서 원하는 값을 어떻게 얻냐?
  • 먼저 해당 하는 값의 타입이 몇번 째 인자로 저장되어 있는지 확인하는 index() 멤버 함수가 있다.
    ==> 0부터 시작
  • 또는 그냥 타입 자체를 알고 싶으면 std::holds_alternative 함수를 사용하면 된다.
    ex) std::holds_alternative<int>(store) 해당 값의 타입이 맞으면 true를 반환 아니면 false
    //store는 variant 객체다.

  • 원하는 값을 얻을려면 std::get<>() 함수를 사용.
    ==> <>에 뽑고자 하는 타입을 쓰든지 아니면 해당 타입의 index를 전달하면된다.
    ==> ()에는 variant객체를 전달.

    • 여기서 한 가지 알 수 있는 점은 varinat 가 보관하는 객체들은 타입으로 구분된다는 점
      ==> 따라서 variant 를 정의할 때 같은 타입을 여러 번 쓰면 컴파일 오류가 난다.
    • variant 에 아무 것도 들고 있지 않은 상태를 표현하고자 싶다면 해당 타입으로 std::monostate 를 사용
      ==> 디폴트 생성자가 있어서 초기화를 안할 때 해당 타입은 첫 번째 타입으로 지정해도 됨(초기화 안할 때)
  • get으로 값을 가져 올 때 저장된 값과 다른 타입을 호출하면 예외가 발생한다.
  • 타입이 일치하지 않을 경우를 대비하여 std::get_if를 사용할 수 있다.
    ==> 타입이 맞으면 해당 값을 포인터로 반환하고 아니라면 nullptr을 반환한다.


tuple

  • 서로 다른 타입들의 묶음을 간단하게 다루는 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;
}
  • std::tuple<> 보관하고 싶은 타입을 <>에 넣어주면 정의하는 방법은 끝이다.
  • vartiant와 다르게 같은 타입이 들어가 있어도 문제가 없다.
  • tuple객체를 초기화를 위해 make_tuple함수를 이용한다.
    ==> 정의했던 tuple 객체에 std::make_tuple(값);

  • 값 사용
  • 값에 접근하는 방법은 std::get<>()으로 <>에 몇 번째 타입에 접근할지, ()에는 tuple객체를 넣어주면 된다.
  • <>에 타입 자체를 넣어서 뽑을 수 있지만 해당 타입이 없거나 2개 이상이면 예외가 발생한다.
    ==> 2개 이상 중에 무엇을 가져와야 할지 몰라서

    또는 tie라는 함수를 이용할 수 있다 ex) std::tie(x,y,z) = tupleValue;
    tuple의 데이터가 각각 x, y, z에 들어가서 1개씩 사용한다.

  • 두 개 이상의 반환값 있으면 포인터나 참조를 이용하거나 구조체를 만들어 전달하는 불편함이 있었다.
  • tuple을 이용한다면 반환값을 몇개이던지 쉽게 표현할 수 있다.

멤버 함수중 2개의 tuple객체의 값을 바꾸는 swap(tuple1, tuple2)함수가 있다
==> 대신 같은 타입과 순서로 정의된 객체여야 한다.

tuple 객체 2개를 연결 시켜주는 std::tuple_cat 함수가 있다.

tuple<int, double> t1(1, 1.1);
tuple<int, char> t1(2, '2');
tuple<int, double, int, char> t3 = std::tuple_cat(t1, t2) // auto로 편하게 표현  가능함

대신 타입을 알맞게 잘 설정해야 한다.


Structured binding

  • 구조체, 컨테이너, 배열 등 어떠한 원소들의 모임에서 변수에 원소를 쉽게 가져오도록 도와준다.
  • C++17 이상
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;
}

tuple에서 get()으로 1개씩 값을 가져오는게 귀찮았다.
하지만 위 처럼 [/ tuple 안에 원소들을 받기 위한 객체/] = tuple객체 로 간단하게 표현, 사용 가능하다.

tie함수로 1개씩 가져올 수 있지만 미리 선언된 변수여야 한다.


  • 여기에서 Structured binding이 가능한 3가지 경우를 말하고 있다.
    1. array
      int arr[3] = { 1, 2, 3 };
      auto [a, b, c] = arr;

      그냥 auto를 사용하면 복사가 일어나고 참조를 하기 위해 auto& 를 사용하면 된다.

    2. tuple-like type

      tuple 같은 타입? 표현식이 std::tuple_size<E>::value 처럼 생긴 친구들은 말한다.

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

      구조체나 클래스 객체에 접근하는 것인데 겍체의 필드가 모두 접근 가능해야 하며 그 중에서 non-static 데이터 멤버에 접근가능하다.



optional
std::nullopt
value_or
variant 참고
std::holds_alternative
tuple
Structured binding
공부한 내용 복습

개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!

0개의 댓글