본문 바로가기
C++/Effective Modern C++

0과 NULL 보다 nullptr

by COCO1337 2020. 6. 30.

리터럴 0은 int이며 포인터가 아니다. 또한 NULL도 포인터 형식이 아니다.

C++98에서 0이나 NULL로 중복 적재 함수를 호출했을 때, 포인터를 받는 중복적재가 호출되는 일은 없다.

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

f(0);		// f(void*)가 아닌 f(int) 호출
f(NULL);	// 컴파일 되지 않을 수도 있지만, 보통은 f(int)호출

f(NULL)의 행동에 관련된 불확실성은 NULL의ㅏ 구체적인 형식을 구현의 재량에 맡긴 결과이다. 여기서 주목할 점은, 소스 코드의 외관상 의미(NULL, 즉 널 포인터로 f를 호출한다)와 실제 의미(널 포인터가 아닌 어떤 정수로 f를 호출한다)가 서로 모순된다는 것이다.


nullptr의 장점은 정수 형식이 아니라는 점이다. 사실 포인터 형식도 아니다. nullptr은 모든 형식의 포인터 라고 생각하면 된다. nullptr의 실제 형식은 std::nullptr_t인데, std::nullptr_t 자체는 다시 nullptr의 형식으로 정의된다. std::nullptr_t는 모든 raw 포인터 형식으로 암묵적으로 변환되며, 이 덕에 nullptr은 모든 형식의 포인터처럼 행동한다.

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

nullptr가 정수 형식으로 해석되지 않기 때문에 f(void*)가 호출된다. 따라서 0이나 NULL대신 nullptr을 사용하면 중복적재가 예상과 다르게 해소되는 일이 없다.

또한 코드의 명확성도 높여준다.

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

findRecord의 반환 형식을 모른다면 result가 포인터형식인지 정수 형식인지 명확히 할 수 없게 된다. 반면

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;

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);		// f2용 뮤텍스를 잠근다
	auto result = f3(nullptr);	// nullptr을 널포인터로서 f3에 전달
}					// 뮤텍스 풀림

반복을 템플릿화 시켜보자

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))	// C++11
{					
	using MuxGuard = std::lock_guard<MuxType>;
	MuxGuard = g(mutex);
	return func(ptr);
}

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);		// 오류!
auto result2 = lockAndCall(f2, f2m, NULL);	// 오류!
auto result3 = lockAndCall(f3, f3m, nullptr);	// OK

처음 두줄은 컴파일 되지 않는데 첫 호출의 문제는, lockAndCall을 0에 넘겨주면 컴파일러가 0의 형식을 파악하기 위해 템플릿 형식 연역을 적용하는데, 0의 형식은 항상 int라는 것이다. 즉, int는 f1이 기대하는 std::shared_ptr<Widget>매개변수와 호환되지 않으므로 컴파일 오류가 난다. 이는 형식 오류이다.

두번째로 NULL을 이용한 호출에서도 비슷하다. NULL이 lockAndCall에게 전달되면 매개변수 ptr는 정수 형식으로 연역되며, std::unique_ptr<Widget>을 기대하는 f2에 ptr을 넘겨주면 형식 오류가 발생한다.

반면, nullptr를 이용한 호출에는 문제가 없다. nullptr가 lockAndCall에 전달되면 ptr의 형식은 std::nullptr_t로 연역된다. ptr를 f3에 전달하면 std::nullptr_t에서 Widget*로의 암묵적 변환이 일어난다.

정리하자면 널 포인터를 지정할 때에는 0이나 NULL이 아니라 nullptr를 사용해야 한다.


Conclusion

- 0과 NULL보다 nullptr를 선호하라

- 정수 형식과 포인터 형식에 대한 중복적재를 피하라


Reference

Effective Modern C++ 항목 8: 0과 NULL보다 nullptr를 선호하라

반응형

댓글