레이블이 Cpp Core Guidelines인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Cpp Core Guidelines인 게시물을 표시합니다. 모든 게시물 표시

2018-02-08

[CppCoreGuidelines] not_null - null이 될 수 없는 값 구분하기

 Null pointer dereferencing은 C++을 사용하다 보면 자주 발생하는 문제다. 값이 없을 수 있는 객체를 지칭할 때 포인터를 사용하고 값이 없는 상태를 null로 표현하는 C++에서 이를 근본적으로 회피할 방법은 없다. 따라서 null일 수 있는 포인터는 사용하기 전에 항상 체크하고 사용해야 한다.

 하지만 모든 포인터가 null이 될 가능성을 가지고 있는 건 아니다. 로직 상으로 일부 포인터들은 null이 될 수 없다. 반드시 존재하는 객체의 주소를 가리키고 있을 수도 있고, 이미 null인지 체크한 포인터일 수도 있다.  이런 포인터까지 사용하기 전에 null인지 체크하고 사용하는 건 귀찮고, 추가 비용만 들어간다.  이런 경우 과거에는 레퍼런스를 이용했다. 레퍼런스는 선언 시 반드시 초기화해야 하므로 레퍼런스가 가리키는 객체는 null이 아닐 것이라는 생각에서였다.  하지만 사실 레퍼런스도 null pointer dereferencing에 대해서 그다지 안전하지 않다. 위와 같은 함수를 아래처럼 포인터를 받아서 부르는 경우를 생각해보자.
위의 코드는 여전히 null pointer dereferencing 문제를 가진다. f5가 인자로 받은 t의 null 체크를 하지 않고 f4로 넘겼기 때문이다.
 게다가 레퍼런스로 부르는 방식은 modern c++에 스마트 포인터가 들어오면서 일반적으로 사용할 수 있는 방법은 아니게 됐다. shared_ptrunique_ptr은 포인터의 semantic을 그대로 따르기 때문에 null이 될 수 있다. 하지만 null이 될 수 없는 shared_ptrunique_ptr를 reference로 표현할 수 없다. 따라서 포인터 시멘틱을 따르는 타입이지만, null이 될 수 없는 객체를 표현할 일반적인 방법이 필요하다.

 C++ Core Guidelinesnull이 될 수 없는 포인터 계열의 변수는 not_null<T>이라는 클래스를 사용하기를 권장한다. not_null<T>은 간단하게 T의 alias일 수도 있다. 하지만 GSL에서는 null 체크를 대신 해주면서 T의 포인터 문법을 그대로 따르는 객체로 구현했다. 이는 실수로 null을 대입하였을 때 빠르게 알기 위해서다. 어쨌든 중요한 점은 not_null<T>이 명시적으로 기존의 T와는 다른 이름으로 보인다는 것이다. 어떤 변수의 타입이 not_null<T>이면, 이 변수에 값을 할당하는 측에서 null 체크를 해야 하고, 이미 할당된 변수에 대해서는 더 이상 null 체크를 안 해도 된다.  간단하게 어떤 함수의 인자가 not_null<T>이라면, 이 인자의 null 체크는 caller가 담당하고, 어떤 함수의 결과 타입이 not_null<T>라면, 이 반환 값의 null 체크는 callee가 담당한다고 말할 수 있겠다.

2018-02-07

[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은 관련 있는 데이터를 하나의 구조체로 묶은 것이다. 매우 기초적이고 단순한 것이지만, 무시해서는 안 된다. 실제로 위의 네 타입만 잘 써도 포인터를 사용하면서 발생하는 많은 문제을 쉽게 해결, 혹은 발견할 수 있다.

2017-11-27

[CppCoreGuidelines] const_cast는 언제 써야 하는가

 C++의 const_cast는 레퍼런싱하는 object의 cv-qualifier를 제거하는 캐스팅이다. cv-qualifier는 타입에 constness와 volatility를 더해주는 한정자이므로 const_cast는 constness뿐 아니라 volatility도 제거할 수 있다. constness를 제거하면 수정할 수 없었던 object를 수정할 수 있게 해주고, volatility를 제거하면 해당 object에 접근하는 코드가 최적화되어 사라질 수 있게 해준다. 따라서 const_cast는 실제 존재하는 object가 별도로 있고, 그것에 접근하는 방법을 변경한다.
 문제는 volatile object의 레퍼런스를 const_cast로 volatility를 없앤 뒤 이 object에 접근하는 것이나, const object의 레퍼런스를 const_cast로 constness를 없앤 뒤 이 object를 수정할 경우 이 코드가 어떻게 동작할지는 undefined behavior라는 것이다. 즉 아래와 같은 코드들은 전부 undefined behavior이다.

 그렇다면 const_cast는 언제 사용하는 것일까? 사실 const_cast를 사용해야만 하는 경우는 없다. 무언가를 하기 위해 반드시 const_cast가 필요하다고 느껴진다면 디자인에 무언가 문제가 있는 것이다. 그래서 C++ Core Guidelines에서는 const_cast를 사용하지 않는 것을 권장한다. 흔히 const_cast를 사용하는 패턴을 정리하면 아래의 3가지 패턴으로 분류할 수 있다. 이제부터 그 3가지 패턴이 왜 사용하면 안 되는지 설명할 것이다.

  첫 번째 용례는 어떤 object의 cv-qualifier를 cv1이고, 이 object를 레퍼런싱하는 cv1보다 더 cv-qualified 된 cv2를 가지는 변수를 가지고 있을 때, cv1보다 높고, cv2보다는 낮은 레벨의 cv-qualifier를 갖도록 하는 것이다. 즉, int i1이 선언돼 있고 이 i1를 지칭하는 const int& i2가 있을 때, i2를 int&로 캐스팅하는 용도다.
 하지만 잘 설계됐다면 undefined behavior를 감수하고 중간에 cv-qualified 된 레퍼런스로 들고 다닐 이유가 없다.

 두 번째는 non-const method를 가지는 클래스의 const instance가 있을 때, 이 instance를 통해 non-const method를 호출하는 것이다.
 하지만 어떤 메소드가 non-const method인 이유는 그 함수 안에서 mutable이 아닌 subobject를 수정하기 때문이다. 이를 const_cast를 통해서 강제로 호출할 경우 subobject를 수정하는 동작이 undefined behavior가 될 수 있으므로 안전하지 않다.

 마지막 사용 방법은 어떤 클래스의 const 메소드와 non-const 메소드 두 개를 정의할 때 코드의 중복을 없애기 위해서다.  이는 Scott Meyers의 Effective C++에서 추천하는 방법이기도 하고 실제로 매우 많이 보게 되는 코드다. 일반적으로 이 경우 const*로 캐스팅한 this가 반드시 non-const object를 레퍼런싱하는 것을 보장하기 때문에 안전하다. 하지만 이는 f 함수가 리턴하는 레퍼런스가 S의 멤버라는 가정하에서만이다. 예를 들어 f가 리턴하는 reference_to_r이 static storage를 가지는 const R이었다면 위의 코드는 정상적으로 컴파일되지만, non-const인 S의 instance를 통해 f를 호출할 경우 그 결과값은 const object를 지칭하는 non-const 레퍼런스이기 때문에 안전하지 않다.
 그래서 C++ Core Guidelines에서는 const_cast의 사용을 금지하고, C++11에서 추가된 decltype을 이용해서 아래와 같이 해결하는 것을 권장한다.  위의 코드에서는 f_impl의 리턴 타입이 템플릿 파라미터인 T에 의존하고, f의 결과값은 f_impl의 결과값이기 때문에, reference_to_r이 const-object의 레퍼런스라면 R& f를 컴파일할 때 에러가 발생하기 때문에 안전하게 선언할 수 있다.

 이상으로 const_cast를 사용하는 코드가 어째서 위험한지 알아보았다. 사실 C++11 이전에도 const_cast가 위험하다는 것은 널리 알려져 있었다. 하지만 코드의 중복을 제거하기 위해 const_cast를 사용하는 것을 대체할 방법이 없었기 때문에, 일반적으로 const_cast를 사용하지 말라고 할 수 없었다. 하지만 C++11에 들어간 decltypeconst_cast 없이도 코드의 중복을 없앨 방법을 제공한다. 따라서 C++11 이후로는 undefined behavior가 되기 쉬운 const_cast를 더는 쓸 이유가 없다.

2017-10-30

[C++] C++ Core Guidelines - modern C++을 위한 안내서

 C++은 사용하기 어려운 언어이다. C++ 이후에 나온 언어들에 비해 사용자에게 너무 많은 자유도를 주기 때문에 안전하게 사용하기 어렵다. 그래서 구글, 웹킷, LLVM 등 많은 스타일 가이드들이 단순히 문법적인 포맷을 어떻게 작성할지 디자인에 대한 부분도 같이 제안한다.

 오늘 소개할 C++ Core Guidelines도 이런 스타일 가이드 문서 중 하나다. modern C++, 즉, C++ 11 이후 C++을 어떻게 사용할지에 초점을 맞힌 가이드 문서로, C++의 창시자인 Bjarne Stroustrup이 주축이 되어 작성되었다. C++ Core Guidelines는 다른 문서보다 C++ 자체를 어떻게 하면 더 안전하게 쓸 수 있을지에 초점을 두기 때문에 문법적인 포맷에 대한 조언은 거의 하지 않는다. 그보다는 어떻게 하면 로직의 문제를 최대한 빠르게, 가능하면 컴파일 타임에 잡을 수 있는지에 대한 디자인 조언을 많이 한다. 개인적으로는 지금까지 읽었던 modern C++ 코딩에 관한 문서 중 가장 실용적인 문서라고 생각한다.

 C++ Core Guidelines를 읽다 보면 GSL이라는 단어가 자주 나온다. C++ Core Guidelines는 반복되는 몇 가지 사용 패턴을 위해서 몇몇 라이브러리를 사용할 것을 권하고 있다. 이를 Guideline Support Library, 줄여서 GSL이라고 부른다. 정의상으로는 가이드라인에서 제안하는 GSL의 스펙을 만족하는 라이브러리는 모두 GSL이라고 부를 수 있다. 하지만 현존하는 구현체는 Microsoft에서 만든 GSL뿐이기 때문에 사실상 Microsoft의 GSL과 같은 의미라고 생각해도 된다.

 앞으로 시간 나는 대로 C++ Core Guidelines와 GSL의 내용 중에서 좋아하는 항목 몇 개를 소개할 생각이다. 하지만 그와는 별개로 앞으로 계속해서 C++ 프로그래머를 할 생각이라면 한 번쯤은 읽어보기를 추천한다.