C++17 표준 라이브러리의 std::optional 소개
이번 글에서는 C++17 표준 라이브러리의 std::optional을 소개합니다.
프로그래밍에서 다음과 같은 시나리오가 흔히 발생합니다.
- 실패할 수 있는 어떤 연산을 수행하는 함수를 호출한다.
- 연산이 성공했으면 함수가 돌려준 결과를 사용하고, 실패했으면 다시 시도하거나 오류로 처리한다.
이러한 시나리오를 구현하려면, 함수가 두 가지 결과를 돌려주어야 합니다. 하나는 연산의 성공 여부이고, 또 하나는 연산의 결과(성공의 경우)입니다. 그런데 C++에서 함수는 많아야 하나의 값만 돌려줄 수 있습니다. 여러 개의 값을 돌려주려면 소위 ‘출력 매개변수’를 이용하거나, 아니면 여러 개의 값을 하나의 객체(이를테면 std::pair
)에 담아서 돌려주어야 합니다. 예를 들어 어떤 정수 옵션 값을 조회하는 함수라면,
// 반환값은 성공 여부이고, 실제 결과는 val에 설정됨.
bool get_option(const std::string& name, int& val);
이나
// pair.first는 성공 여부, pair.second는 실제 값.
std::pair<bool, int> get_option(const std::string& name);
같은 형태가 될 것입니다. 그런데 전자는 구문이 다소 번잡하고, 후자는 연산 실패 시에도 pair
의 둘째 요소를 생성해야 하므로 비효율적입니다(int
대신 덩치 큰 객체라면 그 차이가 의미가 있습니다).
또 다른 옵션은 실패 시 함수가 예외를 던지는 것인데, 지금 예처럼 사용자의 실수가 얼마든지 예상되는, 즉 실패가 일상적인 상황에 예외를 사용하는 것은 뭔가 좀 과하지 않나 싶습니다.
이상의 옵션들보다 좀 더 깔끔하고 편리한 수단이 바로 C ++17에서 표준 라이브러리에 추가된 std::optional
입니다. 이것을 사용하면 이런 코드가 가능합니다.
std::optional<int> get_option(const char* name)
{
// options는 이름-값 쌍들을 담은 전역 std::map 객체라고 가정.
auto match = options.find(name);
if(match != options.end()) {
return match->second;
} else {
return {};
}
}
void f()
{
auto val = get_option("WIDTH");
if (val) {
std::cout << "창 너비:" << *val << std::endl;
} else {
std::cout << "설정 파일 오류" << std::endl;
}
}
get_option()
은 옵션 값을 찾았으면 그 값(int
)을 돌려주는데(return match->second;
), std::optional<T>
에는 const T&&
를 받는 생성자가 있으므로 결과적으로 std::optional<int>
객체가 반환됩니다. 찾지 못한 경우에는 else
절의 return {}
이 실행되며, 그러면 빈(자료가 없는) std::optional
객체가 반환됩니다. 이때 자료(지금 예에서는 int
값)는 생성되지 않습니다. 만일 pair<bool, T>
를 사용했다면, 쓸데없이 T
객체를 생성해야 했겠죠.
f()
의 if (val) ...
에서 보듯이, 부울 값을 요구하는 문맥에서 std::optional
객체 자체는 하나의 부울 값(실제로 자료가 있는지의 여부를 나타내는 )으로 평가됩니다. 객체에 담긴 자료는 앞의 예제의 *val
처럼 역참조를 통해서 얻을 수도 있고, val.value()
처럼 멤버 변수 value()
로 얻을 수도 있습니다.
부울 값을 요구하는 문맥이 아닌 상황에서 명시적으로 부울 값을 얻고 싶다면(이를테면 형식 연역을 위해) 멤버 함수 has_value()
를 사용하면 됩니다. 다음이 그러한 예입니다.
auto val = get_option("WIDTH");
auto r1 = val; // r1은 val의 복사본(std::optional<int> 객체).
auto r2 = val.has_value(); // r2는 bool.
*val
같은 역참조 외에, operator->
를 통해서 자료의 멤버들에 접근할 수도 있습니다. opt_str
이 std::optional<std::string>
객체라고 할 때, opt_str->size()
는 opt_str
에 담긴 문자열의 길이를 돌려줍니다. 그렇다고 포인터의 모든 의미론을 지원하는 것은 아닙니다. 예를 들어 opt_str++
는 불가능합니다.
앞에서 말한 시나리오는 약간 다른, 다음과 같은 시나리오도 흔히 발생합니다(옵션 값 조회라면 이 시나리오가 더 현실적입니다).
- 실패할 수 있는 어떤 연산을 수행하는 함수를 호출한다.
- 연산이 성공했으면 함수가 돌려준 결과를 사용하고, 실패했으면 미리 설정된 기본값을 사용한다.
std::optional
은 이런 시나리오를 위한 수단도 제공하는데, 바로 멤버 함수value_or()
입니다. 이를테면 다음과 같은 코드가 가능합니다.// 설정 파일에서 창의 너비 값을 가져와서 설정하되, // 만일 설정된 값이 없으면 기본값인 800으로 설정한다. window.set_width( get_option("WIDTH").value_or(800) );