ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] 0과 NULL과 nullptr
    C++/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을 사용하는것이 더 바람직합니다.

     

    읽어주셔서 감사합니다.

     

     

    댓글

Designed by Tistory.