리터럴 0은 int 타입이지 포인터 타입이 아니다. NULL 역시 마찬가지로 포인터 타입이 아니다. 그저 암시적으로 널포인터로 해석할 뿐이다.

 

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

f(0); // f(int)
f(NULL); // f(int)

둘 다 매개변수가 int인 함수를 호출하게 된다. 만약 NULL이 0L(long)로 정의되어 있다면 long->int, long->bool, 0L->void* 변환의 우선순위가 모두 동일하기 때문에 중의적 호출이 되어버린다. 어쨌든 코드의 의도와 실행이 모순된다.

 

nullptr은 사실 엄밀히 따지면 포인터 타입도 아니다. 실제 타입은 std::nullptr_t인데 다시 nullptr로 정의될 뿐이다. 그리고 nullptr은 모든 원시 포인터 타입으로 암묵적인 변환이 이루어지기 때문에 마치 모든 타입의 포인터처럼 행동할 수 있다.

 

f(nullptr); // f(void*)

nullptr을 인수로 넘겨줌으로써 의도와 실행이 일치하게 된다.

이뿐만 아니라 auto변수와 같이 사용될 때 코드의 가독성도 좋아지는 역할도 하게된다.

 

여담으로 위의 코드와 같이 함수의 매개변수로 정수 타입과 포인터 타입으로 오버로딩 되어있는 경우에는 중의적인 호출이 이뤄질수 있기 때문에 C++98에서는 정수 타입과 포인터 타입에 대한 함수 오버로딩을 피하라는 지침을 따랐고 nullptr이 등장한 지금도 여전히 유효한 지침이다. 왜냐하면 아직까지도 0과 NULL을 사용하는 개발자들이 여전히 존재할 것이기 때문이다.

 

auto result = findRecord(/* params */);

if (result == 0) { // result는 무슨 타입?
...
}

코드를 보자마자 result의 타입이 정수인지 포인터인지 알 방법이 없다. 함수의 정의를 직접 찾아봐야 알 수 있을것이다.

 

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

하지만 이제는 명확히 알 수 있다. 어떤 타입의 포인터인지는 정의를 봐야겠지만 어쨌든 포인터 타입이라는것은 명백해졌다.

 

nullptr은 템플릿과 함께할 때 더욱 빛난다. 예시를 보면서 얘기해보자.

 

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);
    auto result = f1(0); // 0->nullptr 암시적 변환
}
...
{
    MuxGuard g(f2m);
    auto result = f2(NULL); // NULL->nullptr 암시적 변환
}
...
{
    MuxGuard g(f2m);
    auto result = f3(nullptr);
}

암시적 변환이 일어난건 둘째치고 코드의 중복이 심각하다.

 

template<typename FuncType, typename MuxType, typename PtrType>
decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) // C++14
{
    using MuxGuard = std::lock_guard<MuxType>;
    
    MuxGuard g(mutex);
    return func(ptr);
}
...
auto result1 = lockAndCall(f1, f1m, 0); // error
auto result2 = lockAndCall(f2, f2m, NULL); // error
auto result3 = lockAndCall(f3, f3m, nullptr); // ok

템플릿 타입 추론에 의해서 0, NULL은 널포인터로 암시적 변환이 이루어지지 않는다.

 

auto result1 = lockAndCall(f1, f1m, 0);

함수가 호출될 때 0은 int로 추론되어서 f1의 인수로 넘겨지게 되는데, f1의 매개변수 타입은 std::shared_ptr<Widget>이기 때문에 에러가 발생하게 된다. 하지만 nullptr로 넘겨진다면 lockAndCall에서 std::nullptr_t로 추론되었다가 f3의 인수로 넘겨질 때 Widget* 타입으로 암시적 변환이 이루어지게 되므로 정상적으로 동작하게 된다.

 

대단한 내용은 아니지만 글이 길어졌다. 어쨌든 널 포인터를 지정할 거라면 0이나 NULL대신 nullptr을 사용하라는 얘기이다.

 

◾ 0과 NULL보다 nullptr을 선호하라.

◾ 정수 타입과 포인터 타입에 대한 오버로딩을 피하라.

+ Recent posts