ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스마트 포인터 참조 가이드(Smart Pointer Referece Guide)
    Modern C++/Smart Pointer 2020. 7. 12. 20:11

    1. new를 이용한 동적 객체 생성은 피하되 필요한 경우 스마트 포인터를 사용하라. 

    스택을 사용하고 할당연산자나 이동/복사 연산을 사용하는게 객체의 수명 관리가 편해 코드가 어떻게 동작하는 지 예측하기 편하다.

    동적 할당은 메모리를 힙에 할당하는 데 비용이 드는 것 뿐 만 아니라 메모리의 위치가 연속적이지 않아 컴파일러가 코드의 최적화를 수행하는 데 걸림돌이 되기도 한다. 

    메모리 할당은 필요할 때만 해야하고 new/delete를 직접 사용하지 않고 스마트 포인터를 사용하면 메모리를 직접 관리해야 하는 부담을 줄일 수 있다.  

     

    2. 스마트 포인터 선택 시 가장 먼저 std::unique_ptr 사용을 고려하라.

    동적 메모리 할당이 필요한 경우 우선적으로 std::unique_ptr 사용을 고려한다. std::unique_ptr<>은  커스텀 삭제자를 사용하는 것과 같은 예외적인 경우를 제외하고 new/delete를 사용하는 것과 비용의 차이가 없다. 또한 std::unique_ptr은 std::shared_ptr로 쉽게 변환된다. 

    #include <iostream>
    #include <functional>
    
    struct Drink {
        virtual ~Drink() = default;
        virtual void calculate() = 0;
    };
    
    struct Americano : Drink {
        void calculate() override {
            std::cout << "Coffee price's 5,000 won\n";
        }
    
    };
    
    struct Tea : Drink {
        void calculate() override {
            std::cout << "Tea price's 4,000 won\n";
        }
    };
    
    std::unique_ptr<Drink> make_drink(int type) {
        if(type == 0) {
            return std::make_unique<Americano>();
        } else {
            return std::make_unique<Tea>();
        }
    }
    
    int main() {
        auto coffee = make_drink(0);
        coffee->calculate();
        //std::unique_ptr 의 이동 생성자를 사용해 바로 공유 포인터로 변환
        std::shared_ptr<Drink> tea = make_drink(1);
        tea->calculate();
        return 0;
    }

    팩토리 함수 make_drink() 는 unique_ptr을 반환하는 데 이후 해당 객체를 공유해서 사용하고 싶다면 

    std::shared_ptr<Drink> tea= make_drink(1); 과 같이 shared_ptr의 이동 생성자를 사용하면 된다. 

     

    std::shared_ptr은 제어블록을 동적할당하고 참조 카운터를 위한 atomic 연산을 수행해야 하는 비용이 들게 된다. 또한 커스텀 삭제자를 사용할 경우 추가적인 비용이 든다. 따라서 std::unique_ptr로 객체를 생성하고 필요한 경우 std::shared_ptr을 사용한다. 

    2. 커스텀 삭제자를 사용하지 않는다면 항상 std::make_xxx 함수를 사용하라. 

    #include <iostream>
    #include <functional>
    
    int throw_exception(bool enable) {
        if(enable)
            throw std::runtime_error("error occur");
        return 10;
    }
    
    void foo(std::unique_ptr<int> a, int result){
        std::cout << *a  << ((*a == result)? " = " : " != ") << result << std::endl;
    }
    int main() {
        foo(std::unique_ptr<int>(new int(10)), throw_exception(true));
        return 0;
    }

    foo 함수는 크게 세가지 일을 수행하는데

    1. new int(10)을 통해 동적으로 메모리를 할당하고

    2. 해당 포인터를 관리하는 std::unique_ptr<int> 객체가 생성된다.

    3. throw_exception() 함수를 호출한다. 

    위와 같은 동일한 순서로 foo 함수가 실행되는 경우 문제가 발생하지 않는다. 하지만 컴파일러는 이를 순서대로 실행하는 코드를 만들지 않아도 된다. 만약 실행 코드가

    1. new int(10)을 통해 동적 메모리 할당

    2. throw_exception()  함수 호출

    3. 동적 메모리 포인터를 이용해 std::unique_ptr<int> 객체 생성

    순으로 컴파일된다면 동적 객체에 대한 소멸자를 호출 할 수 없으므로 메모리 누수가 발생하게 된다.  

    int main() {
        //foo(std::unique_ptr<int>(new int(10)), throw_exception(true));
        foo(std::make_unique<int>(10), throw_exception(true));
        return 0;
    }

    std::make_unique 함수를 사용할 경우에는  두 함수의 실행 순서를 살펴 보면 된다. 

    std::make_unique() 함수 호출 되고 throw_exception() 에서 예외가 발생할 경우 

    std::make_unique() 함수는 이미 std::unique_ptr이 객체를 가리키고 있으므로 스마트 포인트 소멸 시 가리키는 객체도 정상적으로 소멸된다. 반대로 익셉션이 먼저 발생하는 경우 객체에. 대한 메모리 할당을 하지 않으므로 문제가 발생하지 않느다. 

    이는 std::shared_ptr에도 동일하게 적용된다. 

     

    std::unique_ptr<Drink> = std::unique_ptr<Americano>(new Americano());
    std::unique_ptr<Drink> = std::make_unique<Americano>();
    

    std::make_uniuqe를 사용할 경우 new 호출을 직접할 필요가 없고 타입을 두번 타이핑할 필요도 없다. 따라서 코드 작성도 편해지고 가독성도 증가한다. 

     

    std::shared_ptr의 경우 공유 포인터가 가리키는 객체에 대한 동적 할당과 제어 블록을 위한 객체에 대한 동적 할당이 각각 발생한다.

    하지만 std::make_shared<T>()를 사용하는 경우 컴파일러는 해당 객체와 제어 블록을 위한 메모리 블록을 한번에 할당하기 때문에 코드의 실행 속도가 빨라진다. 또한 제어 블록에 지칭객체에 관리를 위한 정보를 포함할 필요가 없어 메모리 사용량도 줄어든다.

    물론 make_xxx 함수를 사용한 스마트 포인터에는 제한 사항과 성능 향상을 위해 포기해야할 잇점도 있다. 

    std::make_shared 와 std::make_unique는 둘 모두 커스텀 삭제자를 사용할 수 잆는 방법이 없다. 따라서 커스텀 삭제자를 사용하는 경우라면 두 함수를 사용할 수 없다. 

    std::shared_ptr을 make_shared를 통해서 생성한 경우 지칭 객체와 제어 블록을 한번에 관리하게 되므로 shared_ptr(공유 참조)을 weak_ptr(약한 참조)이 가리키고 공유 포인터의 참조 카운터가 0이 되는 경우 객체 소멸이 지연된다. 이는 제어 블록에 약한 참조에 대한 카운터가 존재하는 데 제어 블록에 약한 참조 카운터가 0 이 아닌 경우 해당 객체를 관리하는 제어 블록을 소멸시킬 수 없는 데. make_shared로 생성된 공유 포인터는 지칭 객체와 제어 블록이 한 번에 관리되므로 제어 블록만을 해제할 수 없다. 

    이런 제한과 기회비용 이 존재하더라도 make_xxx 함수를 사용하면 얻는 이점이 더 많기 때문에 스마트 포인터 사용 시 make_xxx 함수를 사용하는 게 최선의 선택이다. 

     

     

     

     

    'Modern C++ > Smart Pointer' 카테고리의 다른 글

    2. std::shared_ptr 과 std::weak_ptr  (0) 2020.07.12
    1. std::unique_ptr  (0) 2020.07.02

    댓글

Designed by Tistory.