ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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();

    참고 사이트

     

    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

     

    반응형

    댓글

Designed by Tistory.