ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] Pimpl idiom
    C++/Effective Modern C++ 2022. 6. 1. 16:17

    Pimpl 관용구 (Pimpl idiom)은 빌드 최적화 및 클라이언트 의존성 해소를 위한 디자인 패턴 입니다.

     

    이번 글에서는 Pimpl에 대한 기본적인  구현 방식을 살펴보도록 하겠습니다.

     


     

    1. Pimpl

     

    더보기

    Pimpl의 기본적인 구현 방식은 자료 멤버들을 특정 객체로 대체하고, 그 특정 객체를 포인터를 통해 간접적으로 접근하는 것 입니다.

    예제를 살펴보도록 하겠습니다.

    class MyClass {
    public:
        MyClass();
        ...
    private:
        std::string name;
        std::vector<double> data;
        Object obj_1, obj_2; // Custom class
    };

    위와 같이 정의된 클래스가 있을 경우, 클라이언트는 <string>, <vector>, object.h를 포함해야 합니다.

    그리고 이것은 컴파일 시간을 증가시키며, 해당 헤더들에 대한 의존성이 생기게 됩니다.

    의존성이 생기게 될 경우, 해당 헤더들이 변경될 경우 클라이언트도 다시 컴파일 해야합니다.

     

    위와 같은 단점을 해결하기 위해 MyClass객체의 멤버 변수들을 감싸는 새로운 객체를 선언할 수 있습니다.

    class MyClass {
    public:
        MyClass();
        ...
    private:
        struct impl;
        std::unique_ptr<impl> pimpl;
    };
    
    // MyClass.cpp
    #include "MyClass.h"
    #include "Object.h"
    #include <string>
    #include <vector>
    
    struct MyClass::impl {
        std::string name;
        std::vector<double> data;
        Object obj_1, obj_2;
    };
    
    MyClass::MyClass() : pimpl(std::make_unique<impl>()) {}

    위와 같이 헤더파일에 불완전한 형식을 선언하고 코드 파일에 정의할 경우 컴파일 시간을 단축할 수 있습니다.

     

    이러한 Pimpl구현에는 몇 가지 문제점이 존재하는데, 이하 문단에서 그것에 대해 다루도록 하겠습니다.

     

    2. 소멸자

     

    더보기

    첫 문단에서 소개한 MyClass를 사용할 경우 아래와 같은 코드를 떠올릴 수 있을 것 입니다.

    #include "MyClass.h"
    
    MyClass cls;

    하지만 이 코드는 컴파일 되지 않습니다.

    이는 std::unique_ptr가 불완전한 형식을 지원하고 있기 때문입니다.

    조금 더 구체적으로는 아래와 같습니다.

    • 객체의 소멸 시점에 컴파일러가 소멸자를 자동으로 작성합니다.
    • 기본으로 작성되는 소멸자는 std::unique_ptr에 대한 소멸자 입니다.
    • 이 소멸자는 delete연산자를 적용하기 전에, 해당 Raw pointer가 불완전한 형식을 가리키는지의 여부를 static_assert를 이용해 점검합니다.
    • 이 때 검사하는 형식이 불완전한 형식으로 판명되어, 오류가 발생하게 됩니다.

    이것은 소멸자를 정의 및 구현하는 것으로 간단하게 해결할 수 있습니다.

    class MyClass {
    public:
        MyClass();
        ~MyClass();
        ...
    };
    
    // MyClass.cpp
    ...
    MyClass::~MyClass() = default;

    위와 같이 기본 소멸자를 사용하도록 선언하는 것으로 위와 같은 문제를 해결할 수 있습니다. 

     

    3. 이동 생성자

     

    더보기

    Pimpl 관용구로 생성하는 클래스는 이동 연산을 사용하기에 적합합니다.

    하지만 이동 생성자, 이동 배정 연산자는 특수 함수에 속하여, 소멸자가 존재할 경우 자동 작성되지 않습니다.

    특수 멤버 함수에 대한 추가적인 내용은 다른 글 (Special member function)에 정리되어 있습니다.

     

    따라서 이동 연산을 추가적으로 정의해야 합니다.

    class MyClass {
    public:
        ...
        MyClass(MyClass&& rhs);
        MyClass& operator=(MyClass&& rhs);
        ...
    };
    
    // MyClass.cpp
    ...
    MyClass::MyClass(MyClass&& rhs) = default;
    MyClass& MyClass::operator=(MyClass&& rhs) = deafult;

    헤더 파일에 default선언은 유효하지 않습니다.

    이 또한 소멸자의 경우와 동일하게, 불완전한 형식을 소멸시키기 때문에 예외가 발생합니다.

     

    4. 복사 생성자

     

    더보기

    MyClass의 실질적인 멤버 변수인 std::string, std::vector, Object는 복사가 가능한 형식입니다.

    (Object는 구현에 따라 다르겠지만, 여기서는 복사가 합당한 형식이라고 가정하겠습니다.)

    따라서 MyClass객체 또한 복사 연산을 지원하는 것이 합당합니다.

    하지만 pimpl을 감싸고 있는 std::unique_ptr는 이동 전용 형식이기 때문에 복사 연산은 직접 정의해야 합니다.

     

    위 문단의 소멸자, 이동 연산과 유사하게 복사 연산을 정의하면 아래와 같습니다.

    class MyClass {
    public:
        ...
        MyClass(const MyClass& rhs);
        MyClass& operator=(const MyClass& rhs);
        ...
    };
    
    // MyClass.cpp
    ...
    MyClass::MyClass(const MyClass& rhs): pimpl(nullptr) {
        if (rhs.pimpl) pimpl = std::make_unique<impl>(*rhs.pimpl); 
    }
        
    MyClass& MyClass::operator=(const MyClass& rhs) {
        if (!rhs.pimpl) pimpl.reset();
        else if (!pimpl) pimpl = std::make_unique<impl>(*rhs.pimpl);
        else *pimpl = *rhs.pimpl);
        
        return *this;
    }

    위 복사 연산들은 rhs가 널인 경우 (rhs가 이미 이동되었을 경우)를 반드시 처리해주어야 합니다.

    그 외에는 컴파일러가 impl에 대한 복사 연산을 기본으로 작성하는 것을 이용하여 구현되었습니다. 

     


     

    이번 글은 Pimpl을 구현함에 있어 std::unique_ptr를 사용했습니다.

    이 구현을 std::shared_ptr을 사용할 경우, 위의 문제상황 및 해결법이 적용되지 않습니다.

    std::shared_ptr는 이전 글(std::shared_ptr)에서 언급된 것과 같이, 커스텀 소멸자에 대한 적용 방식이 std::unique_ptr와는 다릅니다.

    Pimpl을 구현함에 있어 std::shared_ptr를 사용한다면, 소멸자, 복사 생성, 이동 생성이 모두 자동으로 작성되고 원활하게 작동합니다.

    하지만 Pimpl 관용구의 구현을 위한 impl객체는 MyClass에 대하여 독점적인 소유 관계이기 때문에, std::unque_ptr를 사용하는 것이 합당합니다.

    특별한 설계를 통해 소유권을 공유하는 상황이 생기는 것이 아닌 한, Pimpl의 구현에는 std::unqiue_ptr를 사용하고, 위의 예제를 따라 적용하는 것이 좋겠습니다.

     

    감사합니다.

    댓글

Designed by Tistory.