C++ pimpl과 관련한 이야기들
개요
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();
참고 사이트
PImpl - cppreference.com
"Pointer to implementation" or "pImpl" is a C++ programming technique[1] that removes implementation details of a class from its object representation by placing them in a separate class, accessed through an opaque pointer: // -------------------- // inter
en.cppreference.com
Is the PIMPL idiom really used in practice?
I am reading the book "Exceptional C++" by Herb Sutter, and in that book I have learned about the PIMPL idiom. Basically, the idea is to create a structure for the private objects of a cl...
stackoverflow.com
- unique_ptr 관련 이야기
How to implement the pimpl idiom by using unique_ptr
Expressive code in C++
www.fluentcpp.com