programming/programming general

[C++] EMCP Item 19: 자원에 대한 공유 소유권을 위해서는 shared_ptr을 사용하라

Roien 2018. 3. 25.
반응형

reference count의 performance implications

(참조 횟수 관리는 다음과 같은 성능에 영향)


1) raw pointer의 2배 크기

referencing mechanism을 처리하기 때문

2) reference count 용 memory(Contorl block)는 동작 할당됨

reference count는 객체에 associated

reference count는 동적으로 할당되는 data에 저장됨

3) reference counting은 atomic 이어야 함



unique_ptr과 달리 shared_ptr은 삭제자의 지정 방식이 다름


    auto loggingDel = [](Widget *pw) // custom deleter (as in Item 18)

        {

            makeLogEntry(pw);

            delete pw;

        };



    // deleter type is part of ptr type

    std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel); 


    // deleter type is not part of ptr type

    std::shared_ptr<Widget> spw(new Widget, loggingDel);



shared_ptr은 서로 다른 custom deleter를 지녀도 동일 객체로서 관리(사용)됨


    auto customDeleter1 = [](Widget *pw) { … };  // custom deleters

    auto customDeleter2 = [](Widget *pw) { … };


    std::shared_ptr<Widget> pw1(new Widget, customDeleter1);

    std::shared_ptr<Widget> pw2(new Widget, customDeleter2);


    std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };



control block

    shared_ptr 객체는 reference count 용 pointer를 지님

    control block에 대한 pointer임

    (객체 별 control block이 필요)



control block은

custom deleter의 copy 역시 지님

custom allocator 역시 가능

secondary reference count인 weak count 도 지님



std::shared_ptr<T>

+-----------------+       +----------+

|  Ptr to T       | ----> | T Object |

+-----------------+       +----------+

|  Ptr to Control |

|         Block   | --+   Control block

+-----------------+   |   +-------------------+

                      +-->| Referece Count    |

                          +-------------------+

                          | Weak Count        |

                          +-------------------+

                          | Otehr data        |

                          | e.g., custom      |

                          | deleter/allocator |

                          | etc.              |

                          +-------------------+


   Contrl block은

    최초 shared_ptr의 생성 시 set up 됨



control block의 생성

   1) make_shard 시 생성

   2) shared_ptr에 raw pointer 대입 시 생성


make_shred

   기본 생성자, 소멸자 사용 시 사용 가능



control block의 생성에 대한 규칙


  1) make_shared는 항상 control block을 생성

  2) raw / 고유 소유권 포인터(unique_ptr/auto_ptr)에서 shared_ptr 생성 시

    control block 생성 



control block을 생성하지 않는 경우

    이미 control block을 지닌 객체로부터 shared_ptr 생성은,

    이미 생성된 shared_ptr 혹은 weak_ptr을 생성자 argument로 넘기면 됨

      raw pointer가 아니어야 함

      단, 이 때는 새로운 control block을 생성하지 않음


      raw pointer를 넘길 수 있게 되면,

      여러 contrl block이 가능해 져서 예상치 못한 오류를 발생



하나의 raw pointer에서 여러 개의 shared_ptr을 생성하면, 객체에 여러 개의 control block이 만들어지기에 이상 동작이 발생할 수 있음


bad ex.

    auto pw = new Widget;


    std::shared_ptr<Widget> spw1(pw, loggingDel); // create a control block

    std::shared_ptr<Widget> spw2(pw, loggingDel); // create a control block



raw pointer인 pw에 직접 할당하는 방식은 나쁘다.

철학에 위배


  위 코드에서 *pw는 2개의 reference count를 지님

  결국 2회 제거를 시도할 것임 (2번째 desctuction에서 undefined behavior 발생)



shared_ptr 사용 주의점

  1) shared_ptr에는 raw pointer를 넘기면 안 됨

     대신, make_shared를 사용


   custom deleter의 사용 시 make_shared 사용 불가

   만약 raw pointer를 사용해야만 한다면, 중간에 pointer 변수 사용 없이

   바로 pass 하는 것이 좋음


   std::shared_ptr<Widget> spw1(new Widget, loggingDel);

   std::shared_ptr<Widget> spw2(spw1);


       shared_ptr에 shared_ptr을 치환하면 동일 control block을 사용



  2) 객체 내에서 this를 shared_ptr 생성에 넘기지 말아야 함


    std::vector<std::shared_ptr<Widget>> processedWidgets;


    class Widget {

    public:

        …

        void process();

        …

    };


    void Widget::process()

    {

        …

        processedWidgets.emplace_back(this);  // <-- raw pointer!!

    }


    this에 대해 또다른 shared_ptr이 생성되고 processedWidgets에 들어감



    this를 통해 중복 control block 생성을 막는 방법

    std::enable_shared_from_this 사용

    this pointer로부터 shared_ptr을 안전하게 생성하려면, 


    class Widget: public std::enable_shared_from_this<Widget> {

    public:

      ...

      void process();

    };


    위의 pattern을 CRTP (Curiously Recurring Template Pattern)이라고 함



    enable_shared_from_this의 member function인 shared_from_this를 

    사용해서 control block을 복제하지 않음


    void Widget::process() {

        ...

        processedWidgets.emplace_back(shared_from_this())

    }



Raw 객체가 shared_ptr로 치환 시 새로운 control block을 안 만드는 방법

   enable_shared_from_this을 상속 받음


   class clasname : public std::enable_shared_from_this<classname> {

       ...

   };



   enable_shared_from_this는

       shared_from_this() member function을 지님

       이를 통해 shared_ptr을 생성함 (control block을 생성하지는 않음)



           shared_from_this()는

               내부적으로 현재 객체의 control block을 찾고

               현재 control block을 참조하는 shared_ptr을 생성함



client로부터 shared_from_this를 호출하는 member 함수의 호출을 막기 위해서

   enable_shared_from_this를 상속받는 객체는 종종 생성자를 private에 선언

   대신 객체의 생성은 factory function을 사용


   class Widget: public std::enable_shared_from_this<Widget> {

   public:

       ...

       template<typename... Ts>

       static std::shared_ptr<Widget> create(Ts&&... params);

       ...

       void process();

       ..

   private:

       ... // ctors

   };




control block's cost

  1) 관리 비용

  대체로 몇 word 정도임 (custom deleter 사용 시 좀 더 커질 수 있음)

  control block이 동적 할당되고 살제자와 할당자의 크기는 얼마든지 가능하고,

  가상 함수 mechanism이 사용되고, 참조 횟수가 원자적으로 조작됨


  그러나 이 cost는 크렇게 크지 않음

  대신, 자원의 수명이 자동으로 관리된다는 이점을 얻게 됨


  2) 배열로 처리가 불가능

  shared_ptr은 단일 객체 pointer만 염두해 두고 설계되었음

  std::shared_ptr<T[]>와 같은 연산이 없음


  operator[]가 없기에 색인으로 배열 원소에 접근하려면 포인터 산술에 기초한 어색한 표현식을 도원해야 함




control block은,

   기본적으로 a few words in size (three words in size)

   여기에 custom deleters and allocators들이 붙으면 커짐


   control block도 상속과 virtual function을 사용,

   즉, virtual functon에 대한 cost가 존재


   그래도 shared_ptr은 기능에 비해 resonalble cost임



Exclusive ownership이 필요시에는 unique_ptr이 휠씬 좋은 성택

   unique_ptr은 raw pointer에 성능과 차이가 거의 없음


   shared_ptr은 unique_ptr에서 생성됨 (upgrade가 용이함)

   단, 반대는 쉽지 않음

   shared_ptr에서는 unique_ptr을 얻을 수 없음



shared_ptr의 단점

   array에 사용할 수 없음


   다음을 제공하지 않음

       shared_ptr<T[]>

       operator[]


           []를 하면 awkward 동작을 수행할 수 있음


       shared_ptr은,

           derived-to-base pointer conversion을 지원

           이건 single object에서만 가능

           array에서는 되지 않음


       그래서

       unique_ptr<T[]>은 이런 conversion을 막음


   built-in array에 대한 alternative를 사용

       std::array

       std::vector

       std::string


   dumb array에 smart pointer의 선언은 늘 항상 나쁜 설계의 신호임




Things to remember

  1) 자원의 수명관리를 편하게 함

  2) shared_ptr size는 unique_ptr의 2배 size 

  3) custom deleter 지원

  4) raw pointer에서 shared_ptr을 생성하는 것은 피해야 함



반응형

댓글