-
C++ pimpl과 관련한 이야기들쾌락없는 책임 (공부)/C++ 짜잘이 2022. 2. 16. 23:11반응형
개요
C++에 대한 이해도를 더 높여보기 위해서 Effective C++을 읽던 도중 pimpl 구조라는 흥미로운 키워드를 보게 되었습니다. 구현부는 따로 저장하고 이 객체의 주소를 가리키는 포인터를 사용하겠다는 아이디어입니다. 딱 보기에 객체 간 연산을 할 때 도움이 되지 않을까 하는 생각이 들었는데 이것에 대해서 여기저기 알아보고 정리해본 것들을 적어보겠습니다.
pimpl - pointer to inplementation 의 기본 형태
<widget.h>
class widget { // public members private: struct impl; // 아래 사용을 위한 전방 선언 std::experimental::propagate_const<std::unique_ptr<impl>> pImpl; };
<widget.cpp>
struct widget::impl { // 실제 구현 };
대부분의 경우 이런 식으로 구현이 되고 있습니다. 실제 구현부의 경우 impl에서 구현이 되어 있고 이를 Widget 클래스가 감싸는 형태이죠.
pimpl 구조의 장점은 무엇인가?
1. 컴파일 타임 시간을 줄일 수 있다
헤더 파일에 정의가 있고 cpp 파일에 실제 구현이 있으면 전방 선언을 통해서 컴파일 시간을 줄일 수 있습니다. impl은 따로 분리가 되어 있기 때문에 impl의 코드를 변경한다고 해서 컴파일 시간이 늘어나거나 하는 일이 없습니다.
만약 이게 단일로 정의가 되어 있었다면 내용을 변경할 때마다 사용처에서 재 컴파일이 일어나야 합니다.
쉽게 말하면 헤더만 참조하는 파일은 재 컴파일을 하지 않아 컴파일 시간을 줄일 수 있습니다.
2. 데이터 은닉
일단 구현과 인터페이스가 분리되어 있다보니 구현부를 자세히 보여주지 않게 됩니다. 캡슐화를 깨지 않게 하는데 큰 도움을 주는 것이죠.
3. 바이너리 호환성 (Binary compatibility)
구현부에 내용이 새로 추가가 되어도 X의 레이아웃이 변경되지 않으니 새 기능 추가에 더 안정적일 수 있습니다. pimpl 없이도 가능한 내용이기는 합니다.
4. move의 이점
기존 클래스들은 move 연산을 할 시 비용이 많이 요구되는 반면 이곳에서는 포인터의 이동을 통해서 빠르게 연산을 해줄 수 있게 됩니다.
pimpl 구조의 단점은 무엇인가?
1. 공간을 더 차지하게 된다 (Space Overhead)
당연한 이야기지만 구현부를 한번 감싸게 되는 구조라 오버헤드가 생길 수밖에 없습니다.
2. 함수 호출시 포인터를 사용해야 함 (Access Overhead)
포인터로 따로 빼져있다보니 포인터를 한번 거쳐서 호출을 해야 한다는 단점이 있습니다.
3. 유지보수가 어려움
코드가 여기저기 흩어지는 문제도 있고 수정 시 파일을 보는 시간이 더 늘어나게 됩니다.
pimpl 구조와 스마트 포인터
이런 pimpl 구조는 스마트포인터와 많이 사용하게 됩니다. 만약 객체 간 move연산 등을 하다가 중복 참조를 하게 되고 한 곳에서 delete 연산을 하게 되면 다른 곳은 빈 공간을 참조하는 오류가 생길 수 있습니다. 그래서 unique_ptr과 주로 함께하게 됩니다. shared_ptr을 사용하는 경우도 있는데 소유권이 1개인 것이 관리에 더 적합하기에 찾아본 대부분의 코드는 unique_ptr을 사용하고 있었습니다.
대신 unique_ptr이 전방 선언한 타입에 대해서 자동으로 소멸자를 만들어주지 않으니 이에 대해서 사용자가 소멸자를 정의해줘야 할 필요가 잇습니다. 그리고 move 연산의 경우도 재정의를 해줘야 합니다.
#include <iostream> #include <memory> #include <experimental/propagate_const> // -------------------- // interface (widget.h) class widget { class impl; std::experimental::propagate_const<std::unique_ptr<impl>> pImpl; public: void draw() const; // public API that will be forwarded to the implementation void draw(); bool shown() const { return true; } // public API that implementation has to call widget(int); ~widget(); // defined in the implementation file, where impl is a complete type widget(widget&&); // defined in the implementation file // Note: calling draw() on moved-from object is UB widget(const widget&) = delete; widget& operator=(widget&&); // defined in the implementation file widget& operator=(const widget&) = delete; };
// --------------------------- // implementation (widget.cpp) class widget::impl { int n; // private data public: void draw(const widget& w) const { if(w.shown()) // this call to public member function requires the back-reference std::cout << "drawing a const widget " << n << '\n'; } void draw(const widget& w) { if(w.shown()) std::cout << "drawing a non-const widget " << n << '\n'; } impl(int n) : n(n) {} }; void widget::draw() const { pImpl->draw(*this); } void widget::draw() { pImpl->draw(*this); } widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {} widget::widget(widget&&) = default; widget::~widget() = default; widget& widget::operator=(widget&&) = default; // --------------- // user (main.cpp) int main() { widget w(7); const widget w2(8); w.draw();
참고 사이트
- unique_ptr 관련 이야기
반응형'쾌락없는 책임 (공부) > C++ 짜잘이' 카테고리의 다른 글
C++ make_pair vs {} (0) 2022.03.06 C++ fill_n vs memset, 무슨 차이가 있을까 (0) 2022.02.28 C++ 구조체, 클래스 패딩 (0) 2022.01.24 C++ 에서 증감 연산자 오버로딩, 전위, 후위 연산자를 구별하는 방법 (0) 2022.01.11 cout 과 문자열 포인터 (0) 2021.12.27