-
[Effective Modern C++] 0과 NULL과 nullptrC++/Effective Modern C++ 2022. 3. 30. 15:03
nullptr은 C++11부터 널 포인터를 지정하는 포인터 타입 입니다.
기존에 사용되는 NULL과 0은 본질적으로는 정수라는 점에서 몇 가지 문제점이 발생했습니다.
이번 글에서는 0과 NULL사용 시 발생할 수 있는 문제에 관한 예제 몇 가지를 살펴보도록 하겠습니다.
1. 오버로딩과 nullptr
더보기예제를 살펴보도록 하겠습니다.
#include <iostream> void function(int a) { std::cout << "function:int\n"; } void function(bool a) { std::cout << "function:bool\n"; } void function(void* a) { std::cout << "function:ptr\n"; } int main() { function(0); function(NULL); function(nullptr); }
위 예제에서 function(0)은 당연하게도 function(int)를 호출합니다.
function(NULL)의 경우에는 컴파일러마다 다릅니다. (Visual Studio 2022는 int를, GCC는 컴파일 에러가 발생했습니다.)
function(nullptr)은 function(void*)를 호출합니다.
함수를 오버로딩 할 때 포인터와 정수의 중복을 피하는 지침은 여기에서 비롯되었습니다.
널 포인터를 목적으로 사용한 0 혹은 NULL이 예측되지 못 한 행동을 할 수 있기 때문입니다.
하지만 nullptr은 정수 형식이 아닙니다.
nullptr의 실제 형식은 std::nullptr_t이고, 이 객체는 다시 "nullptr의 형식" 으로 연역되는 순환 구조를 가지고 있습니다. std::nullptr_t는 암묵적으로 다른 모든 포인터로 형 변환이 되며, 이러한 특징 때문에 널 포인터로써 기능할 수 있는 것 입니다.
2. 가독성과 nullptr
더보기예제를 살펴보도록 하겠습니다.
#include <iostream> ... int main(){ auto result = find_something( /* params */ ); if(result == 0){ ... } }
위 예제의 find_something의 반환 형식을 모른다고 가정할 경우, result의 형식이 무엇으로 연역되는 지 확실하게 알 수 없습니다.
이 경우 조건문의 0이 포인터인지, 정수인지 확실해지지 않습니다.
하지만
#include <iostream> ... int main(){ auto result = find_something( /* params */ ); if(result == nullptr){ ... } }
비교 대상을 0이 아닌 nullptr로 변경할 경우, 위와 같은 모호성은 사라집니다.
포인터 형식을 비교하는 코드의 경우, 0은 모호해질 수 있고, NULL은 비교가 잘 못 될 수 있습니다. 하지만 nullptr을 사용하면 코드의 의도도 명확해지고, 포인터를 비교하고자 하는 목적도 달성할 수 있습니다.
3. nullptr과 템플릿
더보기다음과 같은 함수와 뮤텍스를 가정하도록 하겠습니다.
int function1(std::shared_ptr<MyClass> s_ptr); double function2(std::unique_ptr<MyClass> u_ptr); bool function3(MyClass* ptr); std::mutex func1_mux, func2_mux, func3_mux;
위 함수들은 적절한 뮤텍스를 잠근 상태에서만 호출되어야 한다고 가정하겠습니다.
이 경우, 세 함수를 호출한다면 코드는 아래와 같습니다.
using MUX_GUARD = std::lock_guard<std::mutex>; ... { MUX_GUARD g(func1_mux); auto result = function1(0); } ... { MUX_GUARD g(func2_mux); auto result = function2(NULL); } ... { MUX_GUARD g(func3_mux); auto result = function3(nullptr); }
위와 같은 반복적인 패턴은 코드를 템플릿화 하는 것이 좋습니다.
뮤텍스는 위 코드를 템플릿화 하기 위한 장치이므로, 이번 문단의 주제와는 무관합니다.
위 코드를 템플릿화 하면 아래와 같습니다.
template<typename FuncType, typename MuxType, typename PtrType> auto lock_and_call(FuncType func, MuxType mux, PtrType ptr) -> decltype(func(ptr)){ using MUX_GUARD = std::lock_guard<MuxType>; MUX_GUARD g(mutex); return func(ptr); }
위와 같이 템플릿화 할 경우, 함수를 호출하는 코드는 아래와 같이 축약이 가능해집니다.
auto result1 = lock_and_call(function1, func1_mux, 0); auto result2 = lock_and_call(function2, func2_mux, NULL); auto result3 = lock_and_call(function3, func3_mux, nullptr);
이 코드는 축약되었다는 점도 중요하지만, 템플릿화 했다는 점도 중요합니다.
축약되기 전의 코드는 모두 실행되지만, 축약 이후의 코드는 그렇지 않습니다.
축약 이후 템플릿을 통해 호출한 lock_and_call에서 0과 NULL은 정수형 입니다.
정수로 연역된 형식을 포인터를 요구하는 함수에 전달할 수 없어 컴파일 에러가 발생합니다.
예제를 통해 오버로딩 된 함수의 호출, 그리고 템플릿의 형식 연역에 있어 nullptr을 사용하는 것이 바람직하다는 것을 알 수 있었습니다.
기존의 암묵적으로 0, NULL값 등을 이용한 것과 다르게 nullptr은 그 자체가 하나의 포인터로써 기능을 하기 때문에 널 포인터를 사용할 경우에는 0 또는 NULL보다는 nullptr을 사용하는것이 더 바람직합니다.
읽어주셔서 감사합니다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] enum과 enum 클래스 (0) 2022.04.18 [Effective Modern C++] using과 typedef (0) 2022.04.05 [Effective Modern C++] 괄호 ()와 중괄호 {} 그리고 Uniform initialization (0) 2022.02.16 [Effective Modern C++] auto와 std::vector, 그리고 Proxy pattern (0) 2022.02.14 [Effective Modern C++] auto의 사용을 고려해야 할 상황들 (0) 2022.01.10