[EMC++] Item 8. 0과 NULL보다 nullptr을 선호해라
Item 8. 0과 NULL보다 nullptr을 선호해라
포인터를 초기화할 때 사용하던 0과 NULL
C++11 이전에는 포인터를 초기화할 때 0
과 NULL
을 사용했다.
0과 NULL의 한계점
0
과 NULL
의 한계점은 포인터 타입이 아니라는 것이다.
포인터만 사용할 수 있는 위치에서 0
을 사용할 경우 컴파일러는 이를 null pointer로 해석하지만 기본적으로 0
은 int이지 pointer가 아니다.
NULL
도 마찬가지이다. NULL
은 컴파일러마다 정의하는 방식의 차이가 있지만, 일반적으로 0
으로 정의한다.
C에서는
NULL
을((void *)0)
으로 정의하는 경우가 많았지만 C++에서는 다른 포인터 타입과 호환이 되지 않아서0
으로 정의하는 경우가 많다.
NULL
이 int 타입의 0
이든 long 타입의 0
이든 확실히 포인터 타입은 아니다.
포인터 타입과 정수 타입에 대한 오버로딩 문제
0
과 NULL
모두 포인터 타입이 아니기 때문에 포인터 타입과 정수 타입에 대한 함수 오버로딩 과정에서 문제가 발생한다.
void f(int);
void f(bool);
void f(void*);
f(0); // f(void*)이 아닌 f(int)를 호출
f(NULL); // 컴파일되지 않거나 f(int)를 호출
포인터 타입의 매개변수를 가진 함수를 호출하고 싶어도 0
과 NULL
모두 포인터 타입이 아니기 때문에 호출할 수 없다.
따라서 C++98에서는 포인터 타입과 정수 타입에 대한 오버로딩을 지양한다. (C++11에서도 0
과 NULL
을 사용할 수 있으므로 여전히 유효하다.)
nullptr
0
과 NULL
이 가진 문제점을 해결하기 위해 C++11부터 nullptr
이 추가되었다.
nullptr
의 데이터 타입은 std::nullptr_t
이며 모든 타입의 포인터로 암시적 형변환이 가능하다.
즉, nullptr
은 모든 타입의 포인터로 초기화가 가능하다.
int* ptr1 = nullptr;
char* ptr2 = nullptr;
double* ptr3 = nullptr;
void (*func1) (int a, int b) = nullptr;
void (*func2) () = nullptr;
nullptr의 장점
nullptr
은 다음과 같은 장점이 있다.
포인터 타입과 정수 타입에 대한 오버로딩 문제 해결
nullptr
은 포인터 타입이므로 0
과 NULL
을 사용할 때 발생한 포인터 타입과 정수 타입에 대한 오버로딩 문제를 해결할 수 있다.
void f(int);
void f(bool);
void f(void*);
f(nullptr); // f(void*)를 호출
가독성과 코드 안정성이 좋음
nullptr
을 사용하면 해당 변수가 정수 타입인지 포인터 타입인지 명확하게 알 수 있다.
auto result = findRecord(a);
// result가 int 값의 0인지 포인터 타입인지 헷갈릴 수 있음.
if (result == 0) {
}
// result가 포인터 타입인지 알 수 있음.
if (result == nullptr) {
}
템플릿을 구현할 때 유용함
nullptr
은 템플릿을 구현할 때 아주 중요한 역할을 한다.
std::mutex
를 사용하여 잠금 상태에서만 호출해야 하는 함수를 구현해보자.
// 각 함수들은 잠금 상태에서만 호출할 수 있음
int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f3(Widget* pw);
std::mutex f1m, f2m, f3m;
using MuxGuard = std::lock_guard<std::mutex>;
{
MuxGuard g(f1m); // f1용 뮤텍스로 잠금
auto result = f1(0); // 0을 null pointer로 전달
} // 뮤텍스 해제
{
MuxGuard g(f2m); // f2용 뮤텍스로 잠금
auto result = f2(NULL); // NULL을 null pointer로 전달
} // 뮤텍스 해제
{
MuxGuard g(f3m); // f3용 뮤텍스로 잠금
auto result = f3(nullptr); // nullptr을 null pointer로 전달
} // 뮤텍스 해제
포인터를 초기화하기 위해 각 함수들은 각자 다른 0
, NULL
, nullptr
을 사용했지만 코드는 문제 없이 작동한다.
위와 같은 코드 중복을 해결하기 위해 템플릿을 구현해보자.
// C++11 구현, 리턴 타입을 추론하기 위해 후행 반환 타입과 decltype을 사용해야 함(Item 3 참고)
template <typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr)) {
using MuxGuard = std::lock_guard<MuxType>;
MuxGuard g(mutex);
return func(ptr);
}
// C++14 구현, 리턴 타입을 추론하기 위해 decltype(auto)를 사용
template <typename FuncType, typename MuxType, typename PtrType>
decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) {
using MuxGuard = std::lock_guard<MuxType>;
MuxGuard g(mutex);
return func(ptr);
}
auto result1 = lockAndCall(f1, f1m, 0); // 에러
auto result2 = lockAndCall(f2, f2m, NULL); // 에러
auto result3 = lockAndCall(f3, f3m, nullptr); // 정상 동작
템플릿으로 구현한 코드에 0
과 NULL
을 사용하면 오류가 발생한다.
그 이유는 템플릿이 PtrType
을 추론하는 과정에서 0
과 NULL
을 정수 타입으로 추론하기 때문이다.
정수 타입으로 추론된 ptr
을 포인터 매개변수를 사용하는 함수의 인자로 넣어주면 컴파일 에러가 발생한다.
하지만 nullptr
을 사용하면 PtrType
이 std::nullptr_t
타입으로 추론된다.
std::nullptr_t
타입으로 추론된 ptr
을 포인터 매개변수를 사용하는 함수의 인자로 넣어주면 해당하는 타입의 포인터로 암시적 형변환하여 사용할 수 있다.
Google Style Guide에서도 nullptr을 권장한다.