함수의 반환 값
함수를 사용하다 보면 많은 경우, "함수가 성공적으로 수행됐고, 그 결과 값이 어떻게 되었다"라는 내용을 함수 호출자에게 알려야 할 때가 있습니다.
이러한 문제를 해결하기 위한 방법은 다양합니다.
가장 쉽게 떠올릴 수 있는 것은, 함수가 제대로 동작하면 계산 값을 반환하고, 함수가 실패하는 경우 특수한, 그리고 좀처럼 사용되지 않는 값을 반환하는 것입니다.
예를 들어, 함수가 실패하면 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(); // 저장하고 있는 값을 삭제
'C, C++ > 표준 라이브러리' 카테고리의 다른 글
[C++] std::endl이 무엇인가 (1) | 2024.10.29 |
---|---|
[C++] 객체이면서 참조 역할을 하는 reference_wrapper (0) | 2024.09.29 |
[C++] 오버로딩( overloading )을 통한 사용자 정의 데이터 입출력 (0) | 2024.09.24 |
[C++] 균일 초기화( uniform initialization )과 std::initializer_list (2) | 2024.08.31 |
[C++] getline 함수 사용법 (0) | 2024.08.24 |