728x90
반응형

리터럴 0은 int이지 포인터가 아니다. 실용적인 관점에서는 NULL도 마찬가지이다. 그러나 NULL의 경우에는 다소 불확실한 세부사항이 존재한다. 구현이 NULL에 int 이외의 정수 타입을 부여할 수도 있기 때문이다.

이 점이 이야기하는 주된 문제는 포인터와 정수 타입에 대한 오버로딩이 의외의 방식으로 해소된다는 점이었다. 0이나 NULL로 그런 오버로딩 된 함수를 호출했을 때, 포인터를 받는 오버로딩된 함수가 호출되는 일은 없다.

반면 nullptr을 사용하면 오버로딩이 예상과 다르게 해소되는 일이 없다. 그 뿐만 아니라, nullptr는 코드의 명확성도 높여준다.

템플릿의 타입 추론시에도, 0과 NULL은 정수 타입으로 추론하는 반면 nullptr은 포인터 타입으롤 추론한다.

// f의 세 가지 오버로딩
void f(int);
void f(bool);
void f(void*);

f(0); // f(void*)가 아니라 f(int)를 호출
f(NULL); // 컴파일되지 않을 수도 있지만, 보통은 f(int)를 호출한다. f(void*)를 호출 하는 경우는 없다.
f(nullptr); // f(void*)를 호출한다.

//---
// nullptr이 코드의 명확성을 높여주는 예제
auto result = findRecord( /* 인수들 */ );

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

// findRecord의 반환 타입을 모르거나 쉽게 파악할 수 없다면, result가 포인터 타입인지 아니면 정수 타입인지를 명확히 말할 수 없게 된다.
// 반면 다음 코드에는 모호성(ambiguity)이 없다.

auto result = findRecord( /* 인수들 */ );
if (result == nullptr)
{
    ...
}

// 이 경우에는 result가 포인터 타입임이 분명하다.

//---
// nullptr는 템플릿이 관여할 때 특히나 빛난다.
// 적절한 뮤텍스를 잠근 상태에서만 호출해야 하는 함수들이 있는데, 그 함수들이 다음처럼 각자 다른 종류의 포인터를 받는다고 하자.

int f1(std::shared_ptr<Widget> spw); // 이 함수들은 적절한 뮤텍스를 잠그고 호출해야 한다.
double f2(std::unique_ptr<Widget> upw); 
bool f3(Widget* pw);

// 다음은 널 포인터로 이들을 호출하는 예이다.

std::mutex f1m, f2m, f3m; // f1, f2, f3용 뮤텍스들

// C++11 typedef;
using MuxGuard = std::lock_guard<std::mutex>;
...
{
    MuxGuard g(f1m); // f1용 뮤텍스를 잠근다.
    auto result = f1(0); // 0을 널 포인터로서 f1에 전달
} // 여기서 뮤텍스가 풀린다.

...
{
    MuxGuard g(f2m); // f2용 뮤텍스를 잠근다.
    auto result = f2(NULL); // NULL을 널 포인터로서 f2에 전달
} // 여기서 뮤텍스가 풀린다.

...

{
    MuxGuard g(f3m); // f3용 뮤텍스를 잠근다.
    auto result = f3(nullptr); // nullptr을 널 포인터로서 f3에 전달
} // 여기서 뮤텍스가 풀린다.

// 세 경우 모두 뮤텍스를 잠그고, 함수를 호출하고, 뮤텍스를 푸는 패턴을 따른다는 점이 신경에 거슬린다.
// 이런 종류의 소스 코드 중복을 피하는 것이 템플릿의 목적 중 하나이다. 템플릿화 해 보자.

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에서는 다음처럼 만들 수도 있다.
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);
}

// C++11 버전이든 C++14 버전이든 lockAndCall을 이용하면 다음과 같은 코드를 작성할 수 있다.

auto result1 = lockAndCall(f1, f1m, 0); // 오류!
...
auto result2 = lockAndCall(f2, f2m, NULL); // 오류!
...
auto result3 = lockAndCall(f3, f3m, nullptr); // OK

// 0과 NULL의 경우, lockAndCall의 매개변수 ptr의 타입이 정수 타입으로 추론된다.
// 하지만 정수 타입은 스마트 포인터나 Widget* 포인터 타입으로 변환할 수 없다.
// 반면 nullptr의 경우, ptr의 타입은 std::nullptr_t로 추론된다.
// std::nullptr_t는 암묵적으로 모든 포인터 타입으로 변환될 수 있으므로 아무런 문제가 발생하지 않는다.

이 항목에서 말하듯이 nullptr가 더 나은 선택임에도 0과 NULL을 사용하는 개발자들이 여전히 있을 것이기 때문에, 정수 타입과 포인터 타입에 대한 오버로딩도 피해라.

728x90
반응형