[CppCoreGuidelines] 포인터 구분해서 쓰기 - span, owner

C++을 쓰는 사람들이 가장 어려워하는 것 중 하나가 포인터다. 그중에서도 함수 포인터를 읽고 해석하는 것이 가장 어렵다고 한다. 하지만 실제 코드에서는 함수 포인터를 볼 일은 거의 없다. 특히 modern c++에서는 가능하면 std::function를 쓰는 걸 권장하기 때문에 몇몇 특수한 목적을 가진 코드를 제외하고는 함수 포인터를 볼 일은 거의 없다.

그다음으로 어려운 것은 메모리 관리다. C++에서 전통적으로 많이 발생하던 문제가 double free와 memory leak이다. 이는 C++에서 포인터로 가리키는 객체의 소유권이 명확하지 않기 때문이었다. 이를 해결하기 위해 C++11에서는 소유권을 혼자 차지하고 있는 std::unique_ptr과 소유권을 공유하는 std::shared_ptr을 만들었다. std::unique_ptrstd::shared_ptr을 잘 활용하면 dobule free와 memory leak은 예방할 수 있다.

하지만 C++11 이후에도 여전히 포인터는 다양한 역할을 가지고 있다. 현재 C++의 포인터에 남은 역할은 다음과 같다.

  1. std::unique_ptr을 사용하지 않지만, 소유권을 넘길 때
  2. 함수의 인자로 배열을 넘길 때
  3. 문자열을 가리킬 때
  4. 소유권을 넘기지 않고 하나의 객체를 가리킬 때

스마트 포인터를 사용하지 않지만, 소유권을 넘길 때

앞에서 말했듯이 C++11은 std::unique_ptrstd::shared_ptr를 도입하여 소유권을 관리할 수 있도록 하였다. 하지만 스마트 포인터를 사용하지 못하는 경우도 있다. 이 경우 적절한 지점에서 객체를 소멸시켜줘야 한다. 하지만 이를 지칭하는 것이 단순히 포인터이기 때문에, 메모리를 소멸시켰는지, 한 번만 소멸시켰는지 알기 어렵다. 그래서 C++ Core Guidelines에서는 이 경우 owner<T>라는 클래스를 사용하는 것을 권장한다. owner<T> 클래스는 아무런 일도 하지 않는 클래스다. 사실 클래스일 필요도 없기에 GSL에서는 포인터의 alias로 구현하고 있다.

owner<T> 자체는 아무것도 하지 않지만, 포인터와는 구분되기 때문에 코드를 읽는 사람이 소유권을 명확히 알 수 있다는 장점이 생긴다. owner<T>를 인자로 받은 함수가 이 값의 소유권을 넘기지 않았다면 이 함수에서 객체를 소멸시킬 책임을 진다. 다시 말해 이 값을 다른 함수에 넘기거나, 새 스마트 포인터를 만들거나, 리턴하거나, 전역 변수에 할당한 것이 아니라면, 이 함수에서 소멸시켜야 한다.

함수의 인자로 배열을 넘길 때

C++ 11에는 std::array가 들어왔지만, 여전히 배열을 함수의 인자로 사용할 때는 첫 번째 원소의 포인터와 배열의 길이를 따로 넘기는 방법이 많이 사용된다.

이유는 크게 2가지 있다. 일단 아래와 같이 배열이나 std::array를 쓰면, 이 함수는 배열의 길이별로 전부 다른 함수로 instantiation 된다.

또한, 이 함수는 컴파일 타임에 크기가 정해지는 배열에 대해서만 사용할 수 있으므로, 동적 크기 배열에 대해서는 함수를 처음의 방식으로 재정의해야 한다. 이런 문제를 해결하기 위해 C++ Core Guidelines에서는 span<T>이라는 클래스를 정의해서 사용하기를 권장한다. span의 생성자가 배열, std::array, 동적 크기의 배열과 길이를 받는 생성자를 전부 정의하고 있으면, 배열을 받아야 하는 함수는 아래와 같이 span<T>을 이용하면 된다.

span의 여러 가지 장점을 가진다. 우선, 동적 배열과 정적 배열 양쪽에 모두 사용할 수 있다. 또한, 포인터와 길이를 묶기 때문에, 포인터의 길이를 잃어버릴 걱정도 안 해도 된다. 마지막으로 포인터가 지칭하고 있는 것이 배열의 첫 번째 원소인지 단일 객체인지 고민하지 않아도 된다. 실제로 C++ Core Guidelines를 따르는 코드에서는 포인터가 보이면 무조건 단일 객체라고 생각해도 된다.

C-style의 문자열을 가리킬 때

C++에서 포인터를 사용하는 또 다른 용도는 null 캐릭터로 끝나는 C 스타일 문자열을 지칭할 때다. 이 경우 zstring을 이용하면, 이것이 단일 캐릭터인지, 길이를 가지는 문자열인지, null 캐릭터로 끝나는 문자열인지 고민하지 않아도 된다.

C 스타일의 문자열은 zstring을 이용하면 되지만, non-ascii 캐릭터를 사용할 경우 null 캐릭터가 문자열의 마지막을 의미하지 않을 수도 있다. 이 경우 문자열의 길이가 필요하다. 이런 종류의 문자열을 명시하기 위해서 위에서 설명한 span<T>을 사용할 수도 있다. 하지만 C++ Core Guidelines에서는 문자열이라는 것을 더욱 명확히 명시하기 위해서 string_span라는 이름을 사용할 것을 권장한다.

소유권을 넘기지 않고 하나의 객체를 가리킬 때

사실 일반적인 C++ 코드에서 볼 수 있는 포인터는 대부분 소유권을 가지지 않는 하나의 객체를 가리크는 경우이다. 그리고 C++ Core Guidelines를 완벽하게 따르는 코드에서는 포인터를 사용하는 것을 사용하는 유일한 경우다. 따라서 C++ Core Guidelines를 잘 따르는 코드에서 포인터가 보이면 이는 소유권을 가지지 않는 단일 객체를 지칭하는 것으로 생각할 수 있다.

이상으로 C++ Core Guidelines에서 포인터를 어떻게 구분해서 사용하는지 보았다. 보면 알겠지만, owner<T>zstring은 타입에 새 이름을 붙인 것이고, span<T>string_span은 관련 있는 데이터를 하나의 구조체로 묶은 것이다. 매우 기초적이고 단순한 것이지만, 무시해서는 안 된다. 실제로 위의 네 타입만 잘 써도 포인터를 사용하면서 발생하는 많은 문제을 쉽게 해결, 혹은 발견할 수 있다.

댓글

댓글 쓰기

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API