[Effective Modern C++] 8. 0과 NULL보다 nullptr을 사용하자
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
반응형
'Books > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 10. 범위 없는 enum보다 범위 있는 enum을 선호하자 (0) | 2022.08.28 |
---|---|
[Effective Modern C++] 9. typedef보다 별칭 선언을 선호하자 (0) | 2022.08.28 |
[Effective Modern C++] 7. 객체 생성 시 괄호(())와 중괄호({})를 구분하자 (0) | 2022.08.28 |
[Effective Modern C++] 6. auto가 원치 않은 타입으로 추론 될 때에는 명시적 타입의 초기화를 생각하자 (0) | 2022.08.27 |
[Effective Modern C++] 5. 명시적 타입 선언보다는 auto를 선호하자 (0) | 2022.08.15 |
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 10. 범위 없는 enum보다 범위 있는 enum을 선호하자
[Effective Modern C++] 10. 범위 없는 enum보다 범위 있는 enum을 선호하자
2022.08.28 -
[Effective Modern C++] 9. typedef보다 별칭 선언을 선호하자
[Effective Modern C++] 9. typedef보다 별칭 선언을 선호하자
2022.08.28 -
[Effective Modern C++] 7. 객체 생성 시 괄호(())와 중괄호({})를 구분하자
[Effective Modern C++] 7. 객체 생성 시 괄호(())와 중괄호({})를 구분하자
2022.08.28 -
[Effective Modern C++] 6. auto가 원치 않은 타입으로 추론 될 때에는 명시적 타입의 초기화를 생각하자
[Effective Modern C++] 6. auto가 원치 않은 타입으로 추론 될 때에는 명시적 타입의 초기화를 생각하자
2022.08.27