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

류광, 2017/04/18 13:02
이번 글에서는 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)
);
top
트랙백 0 : 의견 # + 0

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

류광, 2017/04/07 18:20
조만간 공식화될 C++17에 추가된 std::string_view를 소개합니다.

std::string_view 클래스[1]는, 제안서(n3921)의 표현에 따르면 “문자열에 대한 비소유 참조(a non-owning reference to a string)”입니다. 좀 더 간단하게 말하면 문자열에 대한 “상수 참조” 또는 “읽기 전용 참조”라고 할 수도 있겠지만, 그렇게 말하면 이 클래스가 const std::string&를 돌려준다는 잘못된 인상을 줄 수 있습니다. 실제로, 원래는 클래스 이름이 string_ref이었지만 그런 오해를 피하자는 취지로 이름이 string_view로 바뀌었습니다.

(이하, 본문에서나 코드에서나 편의상 std::는 생략합니다.)

이 클래스의 주된 용도는 함수의 인수 또는 반환값에 쓰이는 const string&를 대신하는 것입니다. 예를 들어 다음과 같은 함수가 있다고 합시다.

void println(const string& str)
{
    cout << str << endl;
}

println은 상수 참조를 받으므로, 어떤 string 객체 sprintln를 호출하면 문자열이 복사되거나 문자열을 위한 메모리가 새로 할당되는 일이 없습니다. 그러나 다음 경우는 사정이 다릅니다.

println("Hello, world!");

이 호출에서 "Hello, world!"는 C 문자열 리터럴이고, 함수 호출의 맥락에서 이는 결국 const char*로 간주됩니다. printlnconst char*를 받지 않지만, 다행히(?) string에는 const char*를 받는 생성자가 있으므로 호출이 성공합니다. 문제는, 호출 과정에서 string의 생성자가 실행되어서 메모리 할당과 문자열 자료 복사가 발생한다는 점입니다. 궁극적인 목표인 cout << "Hello, world!" << endl;에 비하면 이는 불필요한 비용입니다.

사실 이 경우 cout에(그리고 문자열을 읽기 전용으로 다루는 여러 함수들에) 필요한 것은 문자열의 시작 위치와 문자열 길이뿐입니다(물론 문자의 형식도 필요하지만, 그건 컴파일 시점의 이야기이고요). string_view는 실제 문자열 자료를 소유하지 않고 그 두 정보만으로 하나의 문자열을 지칭합니다. 앞의 println을 다음과 같이 바꾸면,

void println(const string_view& str)
{
    cout << str << endl;
}

이제는 printlnstring 객체로 호출하든 const char*로 호출하든 복사나 메모리 할당이 일어나지 않습니다.

물론 void println(const char*) 중복적재 버전을 추가하거, 더 나아가서 template<typename T> println(T)로 일반화해서 이 문제를 해결할 수도 있겠지만, string_view를 사용할 수 있다면 굳이 그럴 필요는 없겠습니다.

앞의 예제에서 짐작하겠지만 string_viewoperator<<를 지원하며, =, ==, !=, <, >, <=, >=도 지원합니다. 사실 string_viewstd::string의 비수정 인터페이스를 거의 다 지원합니다. 주목할만한 차이점은, string_view에는 c_str() 멤버 함수가 없다는 점과 data()가 돌려주는 것이 널 종료 문자열이 아니라는 점입니다. (stringdata()도 예전에는 널 종료 문자열을 돌려주지 않았지만, C++11부터는 널 종료 문자열을 돌려주도록 바뀌었습니다.)

그리고 string_view만의 인터페이스도 추가되었는데요. 대표적인 것이 멤버 함수 remove_prefixremove_suffix입니다.

constexpr void remove_prefix(size_type n);
constexpr void remove_suffix(size_type n);

이름만 봐서는 문자열을 수정할 것 같지만, 이들은 그냥 string_view가 가진 유일한 정보인 문자열 시작 위치와 길이만 수정할 뿐입니다. remove_prefix는 문자열 시작 위치를 주어진 문자 개수만큼 문자열 끝쪽으로 옮기고, remove_prefix는 문자열의 길이를 주어진 개수만큼 줄입니다. 결과적으로 전자는 문자열의 앞부분, 즉 ‘접두사(prefix)’를 제거하는(remove) 효과를 내고, 후자는 문자열의 ‘접미사(suffix)’를 제거하는 효과를 냅니다.

마지막으로, C++17에는 string_view를 위한 문자열 리터럴도 추가되었습니다. "Hello, world!"sv처럼 끝에 접미사 sv가 붙은 문자열 리터럴은 상수 string_view 객체가 됩니다. (이 접미사를 사용하려면 using namespace std::literalsusing namespace std::string_view_literals가 필요합니다.)


[1] 참고로, 실질적인 기능을 제공하는 것은 std::basic_string_view라는 템플릿이고, std::string_view는 미리 정의된 std::basic_string_view<char>의 별칭입니다. std::basic_string을 설명할 때 흔히 std::string을 예로 드는 것과 같은 맥락에서, 이 글에서도 std::string_view를 예로 사용합니다.

top
트랙백 0 : 의견 # + 0

웹 항해일지, 2017-03-16

류광, 2017/03/16 14:58
오랜만에 웹 항해일지 하나 올립니다.

2년 만의 웹 항해일지입니다. 정보/자료성 페이지도 있고 긴 글도 있습니다.

  • klip.meSENDtoREADER - 첫 긴 글 웹 항해일지를 쓸 때는 InstaPaper와 페이지원의 조합을 사용했는데, 얼마 후 이 둘과 킨들의 조합으로 넘어갔습니다. 데스크톱 크롬의 Send to Kindle (by klip.me)라는 확장기능을 이용해서 klip.me로 글을 보내고, 안드로이드 크롬에서는 공유 기능 + DroidToReader를 이용해서 SENDtoREADER로 글을 보냅니다. 둘 다 결국에는 WIFI를 통해 킨들에 도착하고요.
  • 압축 알고리즘 르네상스 제1부, 제2부 - 신기하게도, 모르는 이야기가 잔뜩 있어도 재미있게 읽은 글입니다. 아주 가끔 아는 이야기가 나올 때마다 반갑네요^^
  • 왜 단순한 설명이 더 나은 설명인가 제1부, 제2부 - 제 홈페이지의 도메인 이름 occamsrazr.net은 이 글에서 말하는 오컴의 면도날(Occam's Razor)에서 따온 것입니다.
  • 이롭게 바탕체 - 요즘 웹 브라우저의 기본 글꼴로 사용하고 있는 글꼴입니다.
  • stackoverflow.com에서 자주 언급된 책들 - 예상했던 책들도 있고 예상치 못한 책들도 있고, 읽은 책도 있고 안/못 읽은 책도 있고 하네요. Modern C++ Design 읽으면서 충격과 공포에 떨던 기억이 납니다...
top
트랙백 0 : 의견 # + 0

2017년 새해 복 많이 받으세요!

류광, 2017/01/07 19:21

제 번역서 독자분들과 occam's Razr 방문객 모두 새해 복 많이 받으세요~

작년 새해 인사 때 게임 개발서를 못해서 아쉬었다고 했는데, 다행히 올해에는 DirectX 11을 이용한 3D 게임 프로그래밍 입문의 후속작인 DirectX 12를 이용한 3D 게임 프로그래밍 입문이 나옵니다. 기대해 주시고요.

올해 초에 GpgStudy.com의 포럼 시스템과 위키 시스템을 갱신할 예정인데(관련 게시물), 잘 진행되면 그 경험을 살려서 이곳 occam's Razor도 업그레이드할 생각입니다.

혹시 기다리신 분이 있을 것 같아서, 올해도 남는 스팀 키 몇 개 뿌립니다. 아무나 하나씩만 가져가시되, 다른 분들이 시간 낭비하지 않도록 등록 성공하면 꼭 답글 남겨 주세요.

게임스팀 키힌트
Two Worlds 2MBM?P-BH9AH-Q3PD5? == 올해 천의 자리
8-bit BoyEQK9X-C8K?H-V4065? == 올해 백의 자리
Deponia4Z4MY-XVJ9?-50YGD? == 올해 십의 자리 - 1
Gorky 17PKN43-VZNM?-MZ45P? == 올해 일의 자리 + 1
top
트랙백 0 : 의견 # + 0

근황 - 2016-12-23

류광, 2016/12/23 20:54
Introduction to 3D Game Programming with Direct3D 12 탈고, GpgStudy 유지보수.

본문 열기

top
TAG 근황
트랙백 0 : 의견 # + 0

◀ PREV : [1] : [2] : [3] : [4] : [5] : ... [58] : NEXT ▶