[C++] EMCP Item 22: pimpl idiom 사용 시 특수 멤버 함수들을 구현 파일에서 정의하라
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에 적용되지 는 않음
댓글