Item 8. 0과 NULL보다 nullptr을 선호해라


포인터를 초기화할 때 사용하던 0과 NULL

C++11 이전에는 포인터를 초기화할 때 0NULL을 사용했다.


0과 NULL의 한계점

0NULL의 한계점은 포인터 타입이 아니라는 것이다.

포인터만 사용할 수 있는 위치에서 0을 사용할 경우 컴파일러는 이를 null pointer로 해석하지만 기본적으로 0은 int이지 pointer가 아니다.

NULL도 마찬가지이다. NULL은 컴파일러마다 정의하는 방식의 차이가 있지만, 일반적으로 0으로 정의한다.

C에서는 NULL((void *)0)으로 정의하는 경우가 많았지만 C++에서는 다른 포인터 타입과 호환이 되지 않아서 0으로 정의하는 경우가 많다.

NULL이 int 타입의 0이든 long 타입의 0이든 확실히 포인터 타입은 아니다.

포인터 타입과 정수 타입에 대한 오버로딩 문제

0NULL 모두 포인터 타입이 아니기 때문에 포인터 타입과 정수 타입에 대한 함수 오버로딩 과정에서 문제가 발생한다.

void f(int);
void f(bool);
void f(void*);

f(0);    // f(void*)이 아닌 f(int)를 호출
f(NULL); // 컴파일되지 않거나 f(int)를 호출

포인터 타입의 매개변수를 가진 함수를 호출하고 싶어도 0NULL 모두 포인터 타입이 아니기 때문에 호출할 수 없다.

따라서 C++98에서는 포인터 타입과 정수 타입에 대한 오버로딩을 지양한다. (C++11에서도 0NULL을 사용할 수 있으므로 여전히 유효하다.)


nullptr

0NULL이 가진 문제점을 해결하기 위해 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은 포인터 타입이므로 0NULL을 사용할 때 발생한 포인터 타입과 정수 타입에 대한 오버로딩 문제를 해결할 수 있다.

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); // 정상 동작

템플릿으로 구현한 코드에 0NULL을 사용하면 오류가 발생한다.

그 이유는 템플릿이 PtrType을 추론하는 과정에서 0NULL을 정수 타입으로 추론하기 때문이다.

정수 타입으로 추론된 ptr을 포인터 매개변수를 사용하는 함수의 인자로 넣어주면 컴파일 에러가 발생한다.

하지만 nullptr을 사용하면 PtrTypestd::nullptr_t 타입으로 추론된다.

std::nullptr_t 타입으로 추론된 ptr을 포인터 매개변수를 사용하는 함수의 인자로 넣어주면 해당하는 타입의 포인터로 암시적 형변환하여 사용할 수 있다.


Google Style Guide에서도 nullptr을 권장한다.


카테고리:

업데이트: