#cpp Pimpl 관용구

2 minute read

컴파일할때 파일 간의 디펜던시를 줄이려고 사용하는 관용구(idiom)이다. public 접근 권한으로 정의된 함수가 변경될 때, 이 파일을 include 하는 녀석들이 다시 컴파일되는 건 당연한데, 다른 녀석들이 못 보는 private 접근 권한의 멤버 함수나 변수가 변경될 때도 같은 일이 일어나는 건 못 참겠다가 Pimpl의 배경이다. 컴파일 시간을 줄이려고 많이 사용하는 관용구. 여러 관용구 중에 가장 많이 사용하는 관용구가 아닐까 생각된다.

// Test.h
struct Pimpl;

class Test
{
public:
    Test();
    ~Test();

    void foo();

private:
    Pimpl*    pImpl;
};

// Test.cpp
#include "Test.h"

//////////////////////////////////////////////////////////////////////////
struct Pimpl
{
    void foo()   {}
};

//////////////////////////////////////////////////////////////////////////
Test::Test() : pImpl(new Pimpl){}
Test::~Test() { delete pImpl;}
void Test::foo() { pImpl->foo();}

기본형은 이렇다. 헤더 파일(.h)에 구현 부분을 모두 때려 박은 Pimpl 포인터를 멤버 변수로 정의하고 소스 파일(.cpp)에서 Pimpl 클래스 혹은 구조체를 정의한다. Pimpl 내부가 미친 듯이 변해도 Test.h에 디펜던시가 걸린 파일들은 다시 컴파일될 필요가 없다.

뭐 장점이야 뻔하다. 컴파일 시간을 줄여준다. 프로젝트가 커질수록 컴파일하는데 걸리는 시간이 감당이 안 될 정도로 길어지는데, 이거 너무 치명적이다. 빌드 시간이 코드를 짜는 흐름을 방해해서 효율을 낮추는 건 자명한 이야기. 이 시간을 줄여주니 완전 땡큐지.

내가 생각하는 가장 큰 단점은 타이핑이 확~ 늘어나서 너무 귀찮고 가독성이 떨어진다는 것이다. 타이핑을 줄일 수 있는 은총알은 없다. 귀찮더라도 Pimpl의 함수를 직접 호출하는 코드를 짜야 한다. 장점이 크니 이 정도는 희생해야 한다. 가독성이 떨어지는 건 Pimpl의 오너인 Test 클래스의 public 멤버 함수 이름과 똑같은 함수를 무조건 Pimpl에 만들고 Test 클래스에서는 다른 짓을 추가로 하지 않고 무조건 동일한 이름의 Pimpl 함수를 호출하는 규칙을 만들면 어느 정도 극복 가능할 수 있을 것 같다. Test는 안 보고 바로 Pimpl을 보면 되니깐.

스마트 포인터 때문에 사용하는 LokiPimpl 헬퍼 클래스가 있어서 살펴봤다. 이 클래스가 이 글을 쓰게 한 원인 되겠다.

// Test.h
class Test
{
public:
    Test();
    ~Test();

    void Foo();

private:
    Loki::PimplOf<Test>::Type pimpl;
};

// Test.cpp
#include "Test.h"

//////////////////////////////////////////////////////////////////////////
namespace Loki
{
    template<>
    struct ImplOf<Test> : public SmallObject<> // inherit SmallObj for speed up
    {
        ImplOf()            {}
        ~ImplOf()           {}
        void Foo()          {}
    };
}

//////////////////////////////////////////////////////////////////////////
Test::Test()            {}
Test::~Test()           {}
void Test::Foo()        { pimpl->Foo(); }

Pimpl의 생성까지 신경 안 써도 되는 게 좋지만 이건 아주 작은거고 하지만 복잡하게 보이고 다른 장점이 안 보여서 사용하기에는 망설여진다. 다른 어떤 장점이 있을까?

헤더 파일에서 Pimpl을 정의하면 이게 변경될 때마다 Test.h를 include한 파일들이 다시 컴파일되므로 말짱 도루묵. 그래서 선언만 하고 소스 파일에서 정의한다. 이 때문에 컴파일 시간에 클래스 크기를 알아야 한다고 징징대는 컴파일러를 달래기 위해 Pimpl의 포인터를 멤버 변수로 정의하게 되는데, 메모리 할당, 해제를 해줘야 하는 게 단점이다. 이 단점은 new와 delete를 구현해서 오버헤드를 줄이는 Fast Pimpl Idiom를 사용해서 극복할 수 있다.

참고