스토리텔링 개발자

[Effective Modern C++] 8. nullptr 본문

Effective C++/Effective Modern C++

[Effective Modern C++] 8. nullptr

김디트 2025. 2. 14. 10:48
728x90

항목 8. 0과 NULL보다 nullptr을 선호하라

 

 

 

0과 NULL
  • 리터럴 0은 int이다.
    • 포인터만 사용할 수 있는 위치에 0을 사용하면 암시적 변환으로 널 포인터로 해석하긴 하지만, 기본은 int.
    • 즉, 포인터가 아니다.
  • NULL 역시 마찬가지다.
    • 하지만, NULL은 int 이외의 정수 타입(long)을 부여할 수도 있으므로 int라고 확신할 순 없다.
    • 아무튼 NULL 역시 포인터 타입이 아니다.

 

 

 

C++98에서의 오버로딩 문제
void f(int);
void f(bool);
void f(void*);

f(0); // f(void*) 가 아닌 f(int)를 호출

f(NULL); // f(void*) 가 아닌 f(int)를 호출하거나 컴파일 에러
  • 만일 NULL의 구현이 int가 아니라면 암시적 변환의 우선순위가 모두 같아서 컴파일 에러
    • long -> int 변환
    • long -> bool 변환
    • 0L -> void* 변환
  • NULL을 사용하면 외관상 의미와 실제 의미가 모순되므로
    • C++98에서는 포인터 타입과 정수 타입을 오버로드 하는 걸 피하라는 지침을 따라왔다.
    • C++11에서도 유효한 지침이긴 하다.
      • 0과 NULL은 여전히 사용 가능하고, 그를 사용하는 개발자가 있을 것이기 때문이다.

 

 

 

nullptr
  • 정수 타입이 아니며, 사실, 포인터 타입도 아니다.
  • 모든 타입의 포인터라고 생각하면 된다.
  • 실제 타입은 std::nullptr_t이다.
    • std::nullptr_t는 다시 'nullptr의 타입'으로 정의된다.(순환 정의)
    • std::nullptr_t는 모든 raw 포인터 타입으로 암묵적 변환된다.
  • 위 예시에서는 반드시 f(void*) 버전이 선택된다.
    • nullptr가 절대 정수 형식으로 해석되지 않기 때문이다.
  • 또한 nullptr는 코드의 명확성을 높여준다.
auto result = findRecord( /* 인수들 */ );
if(result == 0) { ... }
// findRecord의 리턴 타입을 쉽게 파악할 수 없다면 코드 가독성이 나쁘다.

auto result = findRecord( /* 인수들 */ );
if(result == nullptr) { ... }
// 포인터 타입임이 명확해진다.
  • 템플릿을 사용할 때도 명확한 사용이 가능하다.

 

 

 

템플릿과 using 예제
// 아래 함수들은 적절한 뮤텍스를 잠그고 호출해야 함!
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>; // using은 항목 9 참조

{
    MuxGuard g(f1m);
    auto result = f1(0); // 0을 널포인터로 취급하여 f1에 전달. 성공.
}

{
    MuxGuard g(f2m);
    auto result = f2(NULL); // NULL을 널포인터로 취급하여 f1에 전달. 성공.
}

{
    MuxGuard g(f3m);
    auto result = f3(nullptr); // 성공.
}
  • 위 코드의 코드 중복을 없애기 위해 템플릿을 도입한다.
// C++11 버전
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);
}

auto result1 = lockAndCall(f1, f1m, 0); // 컴파일 에러!
auto result2 = lockAndCall(f2, f2m, NULL); // 컴파일 에러!
auto result3 = lockAndCall(f3, f3m, nullptr); // 성공
  • 이제 전달된 매개변수들은 템플릿 타입 추론이 적용된다.(항목 1 참조)
    • 0의 타입은 항상 int로 추론된다.
    • NULL의 타입은 항상 정수 타입으로 추론된다.
    • nullptr의 타입은 항상 std::nullptr_t로 추론된다.
  • 추론된 타입이 매개변수와 일치하지 않으면서 에러가 발생하게 된다.
728x90
Comments