Post List

2015년 3월 28일 토요일

item 08: 0 , NULL 대신 nullptr 을 사용하자.

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점








Effective Modern C++ - Scott Meyers
Item 8: Prefer nullptr to 0 and NULL.

Item 8. 0 이나 NULL 대신 nullptr을 사용하자.

C++ 을 사용하면서 부터 Null Pointer를 표현하기 위해서 NULL을 사용하였다.

#define NULL 0

하지만 NULL은 0의 다른 표현일 뿐이지 Null Pointer 가 아니다.
근데 왜 이렇게 썼을까 ?
과거에는 Null Pointer를 표현 할 수 있는 수단이 없었다.
그래서 어쩔수 없이 NULL로 사용을 하면서,
포인터와 정수형 타입의 오버로딩을 자제하라는 권고안까지 있었다.
왜 자제 하라고 할까 ? 이 문제야 워낙 많이 알려져 있으니깐 PASS 하려 했으나, 그래도 한번 살펴보자.

#define NULL_PTR 0L

void func(int) {};
void func(bool) {};
void func(void*) {};

func(0);           // calls func(int)
func(NULL);        // calls func(int)
func(NULL_PTR);    // calls func(int)
func((void*)NULL); // calls func(void*)

Visual Studio 2015 CTP에서 사용한 함수에 마우스를 살포시 가져가니깐, 같은 오버로딩된 함수를 사용하는 것들의 배경색이 살포시 바뀌었다.
(void*)로 강제로 Casting 한경우를 제외하고는 모두 func(int)가 호출되었다.
int가 아닌 long 타입에 대해서도 func(int)가 호출되었다.
왜 그런지에 대해서는 따로 설명하지 않겠다.
(Overloading Resolution Rule 을 검색하면 친절하게 설명해주는 곳이 많다.)

C++11 에 드디어 nullptr이 등장하였다. (Visual Studio 2010에서도 된다.)
이건 int 타입도 아니고 pointer 타입도 아니다.
std::nullptr_t 타입으로 되어 있으며, 모든 종류의 Raw pointer 타입으로 암묵적 Casting이 가능하다.

func(nullptr); // calls func(void*)

nullptr은 정수형으로 취급 될 수가 없으므로  func(void*)이 정상적으로 호출된다.
이게 nullptr의 장점의 끝일까 ?

Code의 Readability도 높여 줄 수 있다.
특히 auto와 같이 사용했을 때 더더욱 그렇다.

auto OBJ = FindObj();

if (OBJ == 0) { ... }

위 Code에서 FindObj()의 return 타입이 정수형(e.g. int)라고 해석을 할 수도 있다.
물론 다른 그 어떤 타입일 수도 있고...

auto OBJ = FindObj();

if (OBJ == NULL) { ... }

이 Code를 한번 보면... 음...
FindObj()의 return 타입이 Pointer 타입 일 수도 있고... 아니면 다른 어떤 Object의 타입일 수도 있고... 물론 int일 수도 있고...
Code의 다른 부분을 좀 더 봐야 정확히 알 수 있겠단 생각이 든다.

auto OBJ = FindObj();

if (OBJ == nullptr) { ... }

이 Code는 누가 봐도 FindObj()의 return 타입은 Pointer 이다. 다르게 해석이 안된다.

하지만 여전히 nullptr 대신 0 이나 NULL을 사용해도 무방한 Code인건 마찬가지이다.
template 에서 decltype 을 사용하여 인자 값을 이용하여 return 타입을 추론하는 경우,
그냥 딱 봐도 아~ nullptr과 NULL이 다르게 동작하겠구나. 라는 생각이 들 것이다.

구체적으로 예제를 한번 보자.
특정 함수들이 Lock 상태에서만 호출 되어야 하는 경우를 살펴 보자.

int    FuncSP(std::shared_ptr<Widget> SPW);
double FuncUP(std::unique_ptr<Widget> UPW);
bool   FuncRP(Widget* RPW);

std::mutex MS, MU, MR;

using LOCK = std::lock_guard<std::mutex>; // C++11 : 조금 고급진 typedef

{
LOCK L(MS); // RAII 지원하는 lock_guard Code 고급지게
auto RET = FuncSP(0);
}
{
LOCK L(MU);
auto RET = FuncUP(NULL);
}
{
LOCK L(MR);
auto RET = FuncUP(nullptr);
}

역시 nullptr 대신 0 이나 NULL을 사용해도 무방한 Code이다.
근데, 같은 Code가 3번이나 반복되었다.
template을 사용하여 좀 더 고급지게 만들어보자.

template<typename FUNC,
         typename MUTEX,
         typename PTR>
auto LockAndFunc(FUNC F, MUTEX& M, PTR P) -> decltype(F(P))
{
LOCK L(M);
return F(P);
}

C++11 에서는 위와 같이 써야하고 C++14에서는 좀 더 간단하게도 표현이 된다.

template<typename FUNC,
         typename MUTEX,
         typename PTR>
decltype(auto) LockAndFunc(FUNC F, MUTEX& M, PTR P)
{
LOCK L(M);
return F(P);
}

이제 위의 3개의 함수를 호출해보자.

auto RS = LockAndFunc(FuncSP, MS, 0); // error : can't convert int to std::shared_ptr<Widget>
auto RU = LockAndFunc(FuncUP, MU, NULL);    // error
auto RR = LockAndFunc(FuncRP, MR, nullptr); // OK

0 과 NULL은 언제나 정수형으로 추론되기 때문에 std::shared_ptr<Widget>같은 타입으로는 추론 할 수가 없다.
반대로 nullptr은 문제없이 사용할 수 있다.

Things to Remember

* 0 과 NULL은 보다는 nullptr을 사용하자.

* 정수형과 pointer 타입의 overloading은 피하자.

댓글 없음:

댓글 쓰기