C++17 표준 라이브러리의 std::optional 소개

Twitter icon류광, 2017-04-18 13:04
이번 글에서는 C++17 표준 라이브러리의 std::optional을 소개합니다.

프로그래밍에서 다음과 같은 시나리오가 흔히 발생합니다.

  1. 실패할 수 있는 어떤 연산을 수행하는 함수를 호출한다.
  2. 연산이 성공했으면 함수가 돌려준 결과를 사용하고, 실패했으면 다시 시도하거나 오류로 처리한다.

이러한 시나리오를 구현하려면, 함수가 두 가지 결과를 돌려주어야 합니다. 하나는 연산의 성공 여부이고, 또 하나는 연산의 결과(성공의 경우)입니다. 그런데 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_strstd::optional<std::string> 객체라고 할 때, opt_str->size()opt_str에 담긴 문자열의 길이를 돌려줍니다. 그렇다고 포인터의 모든 의미론을 지원하는 것은 아닙니다. 예를 들어 opt_str++는 불가능합니다.

앞에서 말한 시나리오는 약간 다른, 다음과 같은 시나리오도 흔히 발생합니다(옵션 값 조회라면 이 시나리오가 더 현실적입니다).

  1. 실패할 수 있는 어떤 연산을 수행하는 함수를 호출한다.
  2. 연산이 성공했으면 함수가 돌려준 결과를 사용하고, 실패했으면 미리 설정된 기본값을 사용한다. std::optional은 이런 시나리오를 위한 수단도 제공하는데, 바로 멤버 함수 value_or()입니다. 이를테면 다음과 같은 코드가 가능합니다.
    // 설정 파일에서 창의 너비 값을 가져와서 설정하되,
    // 만일 설정된 값이 없으면 기본값인 800으로 설정한다.
    window.set_width(
    get_option("WIDTH").value_or(800)
    );
태그: C++ C++17 표준 라이브러리

comments powered by Disqus