C++ Pimpl

컴파일 타임을 줄이기 위한 방법

Posted by Start Bootstrap on February 15, 2020

PIMPL(Pointer to implementation) 이란

Pimpl은 포인터를 활용해서 선언부에서 데이터 구현 세부를 감추는 프로그래밍 방법입니다. 일반적으로 해당 방법을 사용하는 이유는 두 가지로 압축할 수 있습니다. 하나는 1) 전방 선언을 통한 컴파일 시간 단축과 다른 하나는 2) 인터페이스와 구현의 분리 입니다. 이러한 방식으로 인터페이스 뒤에 실제 클래스 구현을 숨기기 떄문에 모든 구현의 자세한 내용과 종속성이 클래스 인터페이스와 헤더 파일을 오염시키지 않습니다

구현의 핵심은 해당 cpp 파일에서 Pimpl클래스를 선언 하고 정의합니다. 이러한 방식을 통해 구현하게 되면 얻는 이점은 변경 사항이 있더라도 인터페이스가 변경되지 않았기 때문에 클라이언트 코드를 다시 컴파일 할 필요가 없습니다. 해당 구현에 관해서 또 이야기를 하자면 인터페이스 유형의 객체가 구현 유형의 객체 수명을 제어하므로 구현에 대한 포인터는 스마트 포인터(std::unique_ptr 등)이 유용합니다. Scott Meyers는 그의 저서 Modern Effecive C++에서 스마트 포인터를 사용할 것을 추천했습니다. 그의 조언에 따라 unque_ptr로 구현하는 것이 옳다는 생각이 들어서 샘플은 그것으로 작성했습니다.

        
                //header file
                class PIMPLClass /*: private Noncopyable*/  //noncopyable을 명시적으로 붙이는 방법
                {
                public:
                    
                    PIMPLClass();
                    ~PIMPLClass();

                    // movable and noncopyable
                    PIMPLClass(PIMPLClass && op) noexcept;              
                    PIMPLClass& operator=(PIMPLClass && op) noexcept;
                    
                    // copyable이 필요하다면 다음을 구현 아니면 noncopyable을 명시적으로 붙이는 방법도...
                    /*
                    PIMPLClass(const PIMPLClass& rhs);
                    PIMPLClass& operator=(const PIMPLClass& rhs);
                    */

                private:
                    class impl;             // 전방 선언
                    unique_ptr pimpl; // C++11 부터는 원시 포인터 사용보다는 unique_ptr의 사용이 바람직 합니다.
                 };


                 //cpp file

                class impl
                {
                public:
                    //맴버 변수를 여기서 추가 .cpp
                    std::string pimplString;
                    std::string pimpleString2;
                    int         pimpleInt;
                    float       pimpleFloat;
                };

                PIMPLClass::PIMPLClass()
                    : pimpl(std::make_unique())
                {

                }

                PIMPLClass::~PIMPLClass() = default; // 스마트 포인터이므로 여기서 처리할 일이 없습니다.
                PIMPLClass::PIMPLClass(PIMPLClass &&) noexcept = default;
                PIMPLClass& PIMPLClass::operator=(PIMPLClass &&) noexcept = default;
        
    

예시처럼 Header 파일에 impl class 혹은 struct를 전방 선언을 하고 .cpp 파일에서 세부 구현을 정의하는 것입니다. 이러한 방식으로 선언과 정의를 하게 되면 Header에서는 전방 선언을 활용할 수 있어서 컴파일 타임을 줄일 수 있습니다. pimpl을 활용하는 가장 큰 이유 중 하나가 전방 선언을 통해서 컴파일 타임을 줄이는 것입니다. 그리고 인터페이스 구현이 분리가 되었습니다. 따라서 impl의 변수가 변경이 되더라도 컴파일 타임에 영향은 없습니다. 하지만 개발자가 코드를 확인하려면 Header 뿐만 아니라 구현부까지 세세하게 확인해야 한다는 단점이 있습니다.

이렇듯 이러한 메모리 할당의 추가와 코드 복잡성 추가는 같이 따라가는 리스크로 보여집니다. 그리고 디버깅 시에 뎁스가 하나 더 추가되는 것은 불편하긴 합니다. 이러한 장,단점이 있기 때문에 사용이 꼭 필요한 것인지는 항상 고민이 필요합니다. 따라서 장단점을 정리하면 다음과 같습니다.

Props공유 라이브러리의 바이너리 호환성 유지에 도움(Header에 변경 사항이 없으므로) / 인터페이스의 세부 사항 숨기기 / 컴파일 시간의 향상

Corns 더 많은 할당으로 인해 메모리 사용량이 증가 / 유지 보수 노력 증가(세부 구현 사항을 확인하거나 수정하는데 더 노력이 필요합니다) / 성능 손실(내용을 인라인 하지 못할 수 있다고 합니다)

참조 pimpl에 대한 다양한 의견 : https://stackoverflow.com/questions/8972588/is-the-pimpl-idiom-really-used-in-practice