[C++ 17] 함수의 결과와 값을 동시에 알려주는 optional 객체

함수의 반환 값

함수를 사용하다 보면 많은 경우, "함수가 성공적으로 수행됐고, 그 결과 값이 어떻게 되었다"라는 내용을 함수 호출자에게 알려야 할 때가 있습니다.

 

이러한 문제를 해결하기 위한 방법은 다양합니다.

가장 쉽게 떠올릴 수 있는 것은, 함수가 제대로 동작하면 계산 값을 반환하고, 함수가 실패하는 경우 특수한, 그리고 좀처럼 사용되지 않는 값을 반환하는 것입니다.

예를 들어, 함수가 실패하면   std::numeric_limit::max()   가 알려주는 값을 사용하는 것입니다.

그러나, 경험 상 이 방식은 생각보다 불편합니다.

우선, 함수가 실패를 표시하기 위해 어떤 특수한 값을 사용하고 있는지 항상 외울 수 없으므로, 기억이 가물가물하면 코드를 다시 읽어야 합니다.

그 후, 함수 수행 결과를 알기 위해서, 결과 값과    std::numeric_limit::max()   가 반환하는 값을 비교하는 과정을 거쳐야 합니다.

더 심각한 것은, 어떤 경우 계산 결과의 값이   std::numeric_limit::max()   과 같을 수도 있다는 것입니다.

 

예외( exception ) 구문을 사용하는 것을 고려해 볼 수도 있겠습니다.

하지만, 이 방식은 먼저 코드가 복잡해지는 것을 피할 수 없습니다.

try와 catch 블록을 구성해야 하고, 예외가 발생했을 때 이를 알리기 위해 함수 호출자에게 던져야( throw ) 하는 예외에 대해서도 생각을 해봐야 합니다.

물론, 이 방식은 예외가 발생한 원인을 다양한 방법으로 함수 호출자에게 알릴 수 있다는 장점이 있습니다.

그렇지만, 간단한 함수를 호출하기 위해 너무 많은 비용이 드는 것은 사실입니다.

 

다음은 두 int 값을 받아서, 나눈 값을 반환하는 함수입니다.

pair<bool, double> divide( int x, int y){
    
    pair<bool, double> ret;
    if ( y == 0)
        ret.first = false;  // 분모가 0이면 함수 실패
    else{
        ret.first = true;
        ret.second = static_cast<double>(x) / y;
    }
    
    return ret;
}

이 함수의 성공 여부를 알리기 위해 예외 처리를 구현하는 것은 번거롭습니다.

 

이런 경우 가장 많이 사용하는 방법은, 두 개의 값을 반환하는 것입니다.

매개 변수에 참조 변수를 사용하거나, 반환 값으로 std::pair 같은 구조체를 사용하는 방법입니다.

하지만, 이 방식의 단점은, 함수를 구현하는 취향에 따라 함수의 형태가 제각각이라는 것입니다.

그리고, 함수가 실패한 경우에도, 결과 값을 저장할 공간을 준비해둬야 하는 낭비의 문제도 있습니다.

 

C++17 버전에서는 이러한 상황에 잘 들어맞는 클래스를 도입하였습니다.

바로, std::optional <T>입니다.

 

std::optional 소개

optional <T>는 T 타입의 객체를 저장하고, 이 저장 상태를 클라이언트에게 알려주는 기능을 가진 객체입니다.

 

이 객체를 사용하기 위해서 먼저 다음의 헤더 파일을 포함해야 합니다.

#include <optional>

 

이전 항목의 divide 함수를 optionl 객체를 사용하여 수정하면 다음과 같습니다.

#include <optional>
using namespace std;

optional<double> divide( int x, int y){
    
    if ( y == 0)
        return {};  // 분모가 0이면 함수 실패
    else{
        return static_cast<double>(x) / y;
    }
}

이 함수가 제대로 수행되면, optional 객체에 계산 값을 저장합니다.

만약 실패하면, 객체에 값이 저장될 자리를 공백으로 놔둠으로써 실패했다는 것을 알릴 수 있습니다.

  { }   빈 값으로 초기화 )

 

divide 함수를 호출한 클라이언트에서는 함수가 반환한 optional 객체를 아래와 같이 사용할 수 있습니다.

int main(){
    
    optional<double> ret1 = divide( 10 , 2);
    if ( ret1){	// 암시적인 변환
        cout << "result: " << *ret1 << endl;
    } 
    else{
        cout << "division failed\n";
    }

    optional<double> ret2 = divide( 10 , 0);
    if ( ret2.has_value() ){	// 값을 저장하고 있는지 확인
        cout << "result: " << ret2.value() << endl;
    } 
    else{
        cout << "division failed\n";
    }
}

▼출력

result: 5
division failed

예문과 같이 optional 객체를 사용하게 되면, 두 개의 값을 얻기 위해, 추가 인수를 전달하기 위한 변수를 선언해야 한다거나, 반환 값에 여러 값을 담기 위한 구조체를 작성할 필요가 없습니다.

 

optional 사용법

● optional 객체의 초기화는 다음과 같이 할 수 있습니다.

optional<int> opt1{ 5 };
optional<double> opt2 = 3.14;
optional<float> opt3{ };    		// 저장된 값이 없음
optional<char> opt4{ std::nullopt };    // 저장된 값이 없음( 위와 동일 )

 

또한, make_optional 함수를 사용해서 optional 객체를 생성할 수도 있습니다.

이때, optional에 저장될 객체의 타입은 make_optional 함수에 전달된 인수로부터 추론됩니다.

auto opt5 = make_optional(10);  // optional<int>로 추론 가능

 

● 그리고, optional 객체가 값을 저장하고 있는지를 확인하려면 두 가지 방법이 있습니다.

하나는 암시적인 변환 방법입니다.

optional 객체는 bool 값으로 변환을 지원하므로, 아래와 같이 ret1 표현식을 if 구문에 사용할 수 있습니다.

int main(){
    
    optional<double> ret1 = divide( 10 , 2);
    if ( ret1){		// 암시적인 변환
        cout << "result: " << *ret1 << endl;
    } 

    optional<double> ret2 = divide( 10 , 0);
    if ( ret2.has_value() ){	// 명시적인 확인
        cout << "result: " << ret2.value() << endl;
    } 
}

다른 하나는 두 번째 예문처럼 optional 클래스의 has_value 멤버 함수를 사용하는 것입니다.

 

● optional 객체에 저장된 값을 사용하려면 아래와 같이 할 수 있습니다.

if ( ret1){	// 역참조 연산자
    cout << "result: " << *ret1 << endl;
}

첫 번째는 역참조( dereference ) 연산자   *  를 사용하는 것입니다.

만약, ret1 객체가 저장된 값을 갖고 있지 않으면, 이 연산은 정의되지 않으므로, 위와 같이 먼저 값의 유무를 확인해야 합니다.

 

또한,    ->   연산자를 통해, 저장된 객체의 멤버에 접근할 수 있습니다.

이러한 연산자들은 포인터( pointer )의 연산자와 형태와 기능이 같습니다.

optional<string> opt6{ "Hello. world!" };
int sz = opt6->size();  // -> 연산자
cout << "string length: " << sz << endl;

 

두 번째는 value 함수를 사용하는 것입니다.

if ( ret2.has_value() ){	// 멤버 함수 사용
    cout << "result: " << ret2.value() << endl;
}

만약, ret2 객체가 저장된 값을 갖고 있지 않으면, std::bad_optional_access 타입의 예외를 발생시킵니다.

 

세 번째는 value_or 함수를 사용하는 것입니다.

double default_val = 10.0;
cout << "result: " << ret2.value_or( default_val ) << endl;

이 함수는 ret2 객체가 값을 갖고 있지 않으면, 인자로 주어진 기본 값을 반환합니다.

 

● 그리고, optional 객체가 저장하고 있는 데이터를 삭제하려면 reset 함수를 호출합니다.

ret2.reset();	// 저장하고 있는 값을 삭제

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유