[C++] std::optional : 값이 있을 수도, 없을 수도

[C++] std::optional : 값이 있을 수도, 없을 수도

안녕하세요. 이번 글에서는 C++17에서 추가된 std::optional에 대해 알아보겠습니다.

함수가 값을 찾지 못했을 때 무엇을 돌려줄지는 늘 고민되는 부분입니다. -1이나 nullptr 같은 특수한 값에 의미를 맡기면 호출하는 쪽에서 약속을 외워야 하고, 실수하기도 쉽습니다. std::optional은 “값이 있을 수도 있고 없을 수도 있다”는 사실을 타입으로 표현해 이 문제를 해결합니다.

헤더와 기본 사용법

헤더는 <optional>이며 C++17 이상에서 사용할 수 있습니다. 주요 멤버는 다음과 같습니다.

멤버설명
has_value(), operator bool값이 들어 있는지 확인
value()값을 반환, 비어 있으면 std::bad_optional_access 예외
operator*, operator->값에 접근, 비어 있으면 미정의 동작
value_or(기본값)비어 있으면 기본값을 반환
reset()값을 비움
emplace(…)값을 제자리에서 생성
std::nullopt“값 없음”을 나타내는 상수

before / after : 특수 값 대신 optional

먼저 -1을 “찾지 못함”으로 쓰는 기존 방식입니다.

// before : -1이 "없음"이라는 약속을 호출하는 쪽이 알아야 합니다.
int FindIndex(const std::vector<int>& v, int target)
{
    for (size_t i = 0; i < v.size(); ++i)
        if (v[i] == target)
            return static_cast<int>(i);
    return -1;
}

같은 함수를 optional로 바꾸면 반환 타입만 보아도 실패할 수 있음을 알 수 있습니다.

#include <optional>
#include <vector>
#include <iostream>

// after : 값이 없을 수 있다는 사실이 타입에 드러납니다.
std::optional<size_t> FindIndex(const std::vector<int>& v, int target)
{
    for (size_t i = 0; i < v.size(); ++i)
        if (v[i] == target)
            return i;
    return std::nullopt;
}

int main()
{
    std::vector<int> v{ 10, 20, 30 };

    if (auto idx = FindIndex(v, 20))
        std::cout << "index = " << *idx << '\n';   // index = 1
    else
        std::cout << "not found\n";

    std::cout << FindIndex(v, 99).value_or(0) << '\n';  // 0
}

값 접근 방법

value()는 비어 있을 때 예외를 던지고, operator*는 검사하지 않으므로 반드시 값이 있는지 먼저 확인한 뒤 써야 합니다.

std::optional<std::string> name;

std::cout << name.has_value() << '\n';        // 0
std::cout << name.value_or("unknown") << '\n'; // unknown

name = "Alice";                                // 값 대입
std::cout << *name << '\n';                    // Alice
std::cout << name->size() << '\n';             // 5

name.reset();                                  // 다시 비움

try
{
    std::cout << name.value();                 // 예외 발생
}
catch (const std::bad_optional_access& e)
{
    std::cout << "empty: " << e.what() << '\n';
}

주의할 점

  • optional은 값을 내부에 직접 보관하므로 동적 할당이 없습니다. 대신 객체 크기가 값 크기에 bool 하나만큼(정렬 포함) 더 커집니다.
  • 참조는 담을 수 없습니다. std::optional<T&>는 만들 수 없으며, 필요하면 std::optional<std::reference_wrapper<T>>나 포인터를 씁니다.
  • value_or는 기본값을 항상 평가합니다. 기본값을 만드는 비용이 크다면 if 문으로 분기하는 편이 낫습니다.
  • “값 없음”이 오류의 이유까지 알려 주지는 않습니다. 실패 원인이 필요하다면 예외나 오류 코드, C++23의 std::expected를 고려합니다.

정리

std::optional은 값이 없을 수 있음을 타입으로 드러내어 특수 값에 의존하던 코드를 더 안전하게 만들어 줍니다. 값 접근 전에는 has_value()나 if 조건으로 확인하고, 기본값이 있다면 value_or를 쓰면 간결합니다.

이상으로 std::optional에 대해 간단히 알아보았습니다.

참고