카테고리 없음

[C++] EMCP Item 22: pimpl idiom 사용 시 특수 멤버 함수들을 구현 파일에서 정의하라

Roien 2018. 3. 25.
반응형

  Pimpl (Pointer to implementation) Idiom

  When using the Pimpl Idiom, define special member functions in the implementation file.


pimple 장점

  1) compile 속도

  2) 헤더들의 내용에 바뀌어도 client에는 영향을 주지 않음


pimple 특징

  - 불완전 형식임 (uncomplete type)

    선언만 하고 정의는 하지 않는 형식



pImpl의 제대로된 사용

   Impl class or struct

   보통 struct를 선호

   Impl 앞에 prefix 필요 없음

   간단하게 선언함

   변수명도 pImpl 등으로 간단하게 선언


   raw pointer는

       강제 casting, 생성/해제의 miss 등 주의 깊게 사용해야 하기에,

       smart pointer 사용 권장

       unique_ptr 사용

       이를 통해 다른 class에 member 공유 시에도 shared_ptr로 resource leak을 막을 수 있음




build time의 증가 문제

class의 data member들을 class의 pointer 구현 및 struct의 구현으로 치환하는 기법임


primary class 내에서 사용되는 class의 data member를 implementation class로 넣고 이런 data member들의 access를 pointer를 통해 indirectly 접근 하는 것.



class Widget {          // in header "widget.h"

public:

   Widget();

   …


private:

   std::string name;

   std::vector<double> data;

   Gadget g1, g2, g3;  // Gadget is some user-

};                      // defined type



class 정의 부에는 Gadget과 std vector 등을 include 해야 함

혹은 Widget의 client에서 먼저 include 해야 함


이런 header들은 compilation time을 증가시킴

cliet에게 다른 dependency를 지움

   . Widget의 header 변경 시 client 역시 recompile 되어야 함



C++98에서 Pimpl Idiom 적용 시,


   class Widget { // still in header "widget.h"

   public:

       Widget();

       ~Widget(); // dtor is needed?see below

       …

   private:

       struct Impl; // declare implementation struct

       Impl *pImpl; // and pointer to it

   };



client는 더 이상 string, vector 등을 include 할 필요가 없음

compilation time을 줄이고, client에 direct dependency를 낮춤


선언은 되었으나 (declared) 정의가 안된 (not defined) type을 imcomplete type이라고 함 Widget::Impl이 그런 type임



Pimpl Idiom

   1) incomplete type의 pointer member 선언

   2) 동적 할당 및 해제



   #include "widget.h" // in impl. file "widget.cpp"

   #include "gadget.h"

   #include <string>

   #include <vector>


   struct Widget::Impl { // definition of Widget::Impl

       std::string name; // with data members formerly

       std::vector<double> data; // in Widget

       Gadget g1, g2, g3;

   };


   Widget::Widget()        // allocate data members for

       : pImpl(new Impl)   // this Widget object

   {

       ...

   }


   Widget::~Widget() // destroy data members for

   {                 // this object

       delete pImpl;

   }



there are reeks of a bygone millennium in C++98 code !

. it uses raw pointers

. and raw new and raw delete


smart pointers are preferable to raw pointers

std::unique_ptr


replacing the raw pImpl pointer with a std::unique_ptr



   class Widget {          // in "widget.h"

   public:

       Widget();

       …

   private:

       struct Impl;

       std::unieuq_ptr<Impl> pImpl;



구현 file


   #include "widget.h" // in "widget.cpp"

   #include "gadget.h"

   #include <string>

   #include <vector>


   struct Widget::Impl { // as before

       std::string name;

       std::vector<double> data;

       Gadget g1, g2, g3;

   };


   Widget::Widget()

       : pImpl(std::make_unique<Impl>())

   {

       ...

   }


       // unique_ptr의 생성은

       // make_unique를 사용



이제, unique_ptr은 자신이 가리키는 객체를 자동으로 삭제한다.


Widget destructor는 더 이상 필요하지 않음

unique_ptr이 파괴 될 때, unique_ptr은 자동으로 pointing하는 객체를 제거함

   -> 더 이상 명시적인 제거를 할 필요가 없음



   #include "Widget.h"

   Widget w;       <-- ERROR



unique_ptr에 대한 destructor를 정의하지 않았기 때문

Compiler가 생성하는 destructor에서 pImpl의 destructor를 호출


   unique_ptr의 default deleter를 호출

   그 기본 삭제자는 unique_ptr안에 있는 raw pointer에 대해 delete를 적용하는 함수


implementation은 보통 default deleter를 지님

C++11의 static_Assert로 raw pointer가 imcomplete type을 point 하지 않음을 확인함


그래서

Widget w에 대해 static_assert를 만나고 fail 하게 됨

The message itself often refers to the line where w is created, because it’s the source code explicitly creating the object that leads to its later implicit destruction.


Widget에 소멸자를 선언하면 compiler는 자동으로 이동 연산자들을 작성하지 않음




   class Widget {      // as before, in "widget.h"

   public:

       Widget();

       ~Widget();      // <---- declaration only

       …

   private:            // as before

       struct Impl;

       std::unique_ptr<Impl> pImpl;

   };



구현 파일


   #include "widget.h" // as before, in "widget.cpp"

   #include "gadget.h"

   #include <string>

   #include <vector>


   struct Widget::Impl { // as before, definition of

       std::string name; // Widget::Impl

       std::vector<double> data;

       Gadget g1, g2, g3;

   };


   Widget::Widget()        // as before

       : pImpl(std::make_unique<Impl>())

   {

       ...

   }


   Widget::~Widget()   // ~Widget definition

   {

       ...

   }



   Widget의 구현 file에 존재 해야 하기 때문에,


   Widget::~Widget() = default;    // 위와 동일 효과



Pimpl Idiom을 사용하는 class도 move support를 할 수 있음

Item 17에서의 설명처럼, Widget 내에서의 desctuctor의 선언이 compiler가 move operation의 생성을 막음


move를 하고 싶다면, 직접 함수들을 선언해야 함



   class Widget {          // still in

   public:                 // "widget.h"

       Widget();

       ~Widget();

       Widget(Widget&& rhs) = default; // right idea,

       Widget& operator=(Widget&& rhs) = default; // wrong code!

       …


   private: // as before

       struct Impl;

       std::unique_ptr<Impl> pImpl;

   };


위의 접근은 destructor ㅇ의 선언 없는 class와 동일한 문제를 야기

같은 이유로..


compiler-generated move assignment operator는 재 할당 전에 pImpl에 의해 pointing 되는 객체에 대한 파괴가 필요함


그러나 Widget header file내에 pImpl은 imcomplete type을 point


이런 상황이 move constructor에 대해서는 다름

compiler는 전형적으로 move 생성자 내에서 exception 발생 시의 event 내에서 pImpl을 제거하기 위한 코드를 생성함

그리고 pImpl을 제거하는 것은 Pmpl이 complete 해야 함을 요구



문제가 이전과 동일하기에

move operations의 정의를 implementation file로 이동하야 함



   class Widget {          // still in "widget.h"

   public:

       Widget();

       ~Widget();


       Widget(Widget&& rhs);            // declarations

       Widget& operator=(Widget&& rhs); // only

       …

   private: // as before

       struct Impl;

       std::unique_ptr<Impl> pImpl;

   };


구현 파일


   #include <string>       // as before,

   …                       // in "widget.cpp"


   struct Widget::Impl { … };  // as before


   Widget::Widget()            // as before

       : pImpl(std::make_unique<Impl>())

   {}


   Widget::~Widget() = default; // as before


   Widget::Widget(Widget&& rhs) = default; // defini-

   Widget& Widget::operator=(Widget&& rhs) = default; // tions



Conceptually, 이 idiom의 사용은 class가 반영하는 것을 바꾸지는 못함


member의 복사를 위해서는

복사를 위한 function들을 직접 작성해야 함

compiler는 unique_ptr과 같은 move-only type을 지닌 이러한 class에 대한 copy operation들을 만들지 않기 때문



우리는 deep copy를 원하지만, 생성된 함수들은 단지 unique_ptr을 shallow copy 하기만 함



현재 익숙한 절차에 따르면,

함수들은 header에 선언하고, 구현은 구현 file에



   class Widget {      // still in "widget.h"

   public:

       … // other funcs, as before


       Widget(const Widget& rhs); // declarations

       Widget& operator=(const Widget& rhs); // only


   private: // as before

       struct Impl;

       std::unique_ptr<Impl> pImpl;

   };


구현,

   #include "widget.h"     // as before,

   …                       // in "widget.cpp"


   struct Widget::Impl {   // as before

       …

   };


   Widget::~Widget() = default; // other funcs, as before


   Widget::Widget(const Widget& rhs) // copy ctor

       : pImpl(std::make_unique<Impl>(*rhs.pImpl))

   {


   }


   Widget& Widget::operator=(const Widget& rhs) // copy operator=

   {

       *pImpl = *rhs.pImpl;

       return *this;

   }


두 함수 구현들은 conventional.

각각 경우에서, 우리는 단순히 Impl struct의 field들을 source 객체인 rhs에서 destination 객체인 *this로 copy


field들을 하나 하나 copy 하는 것 대신, compiler가 Impl을 위한 copy operator들을 생성하는 것을 이용


이런 operation들은 각각의 field를 자동으로 copy


즉 우리는 Widget::Impl의 compier 생성 copy operation들을 호출하여 Widget's copy operation들만 구현



copy constructor에서 Item 21에서 다뤘던 내용을 따르고 있음

new의 사용 대신 make_unique의 사용



Pimpl Idiom의 구현에 있어서, 객체 내 pImpl은 exclusive ownership을 가지고 있기에 unique_ptr과 같은 smart pointer의 사용을 해야 함


unique_ptr 대신 shared_ptr을 사용한다고 하면,

이 item의 사용에 대한 이점이 더 이상 적용되지 않음


Widget의 destructor를 선언할 필요는 없을 수 있음

user-declared destructor 없이 compiler는 move operation들을 생성함 이것이 바로 우리가 원하는 것임

즉, 아래 코드에서..


   class Widget { // in "widget.h"

   public:

       Widget();

       … // no declarations for dtor

       // or move operations

   private:

       struct Impl;

       std::shared_ptr<Impl> pImpl; // std::shared_ptr

   }; // instead of std::unique_ptr


이것의 cient code는 다음과 같을 것임


   #includes "widget.h"


   Widget w1;


   auto w2(std::move(w1)); // move-construct w2


   w1 = std::move(w2);     // move-assign w1



모든 것이 희망한 것대로 compile되고 동작함


w1은 기본 생성자에 의해 생성됨

이 값이 w2로 이동

이 값은 다시 w1으로 이동


그리고

w1과 w2는 파괴됨 (즉, 이는 Widget::Impl 객체가 파괴됨을 야기)



pImpl에 있어서 unique_ptr과 shared_ptr의 동작상의 차이는

이 smart pointer들이 custom deleter를 지원하는 방법의 차이에서 기인함


unique_ptr의 경우,

delete의 type은 smart pointer의 type의 일부임

이건 compiler가 더 작은 runtime data structure를 만들게 하고 code를 더 빠르게 만듦

결과적으로 이런 pointed-to type들은 compiler-generated special functions(e.g., destructors or move operations)들이 사용될 때 반드시 완벽(충복)해야 함


shared_ptr의 경우,

delete는 smart pointer의 일부가 아님

즉, 이는 보다 큰 runtime data structure를 요구하며 code를 다소 느리게 만듦

그러나 pointed-to type들은 compiler-generated special function들이 employed될 때 완벽할(충족할) 필요가 없음



Pimpl Idiom의 trade-off

   unique_ptr과 shared_ptr간 trade-off

   Widget class와 Widget::Impl class간의 관계는 exclusive ownership이며, unique_ptr이 보다 적절한 tool임


   그럼에도 불구하고, shared ownership이 존재하는 경우,

   shared_ptr이 더 적절한 design choice임


   there’s no need to jump through

the function-definition hoops that use of std::unique_ptr entails.


   unique_ptr의 사용이 수반하는 함수 정의에 대해 시키는 대로 따를 필요는 없음


           jump through a hoop [hoops]

               어떤 명령에나 따르다, 시키는 대로 하다



Things to remember

   . Pimpl Idiom

       build time을 줄여줌

        - compilation dependencies를 제거하여

   . unique_ptr pImpl pointer에 대해

       class header 내에 special member functions(destructor와 move operations)를 선언

       그러나 구현은 구현 file에서  

       default 함수도 구현해야 하는 경우에도 이 rule을 따름

   . 위 advice는 unique_ptr의 경우에 적용됨

       shared_ptr에 적용되지 는 않음

반응형

댓글