728x90
반응형

Pimpl이란 pointer to implementation을 의미한다.

// 기존
class Widget
{
public:
    Widget();
    ...
private:
    std::string name;
    std:;vector<double> data;
    Gadget g1, g2, g3;
};


// Pimpl
class Widget
{
public:
    Widget();
    ~Widget();
    ...
private:
    struct Impl;
    Impl *pImpl;
};

Pimpl 적용 코드를 보면 string, vector, Gadget을 선언하고 있지 않아 #include 할 필요가 없어져 컴파일 속도가 빨라지게 되었다. 이렇게 선언만 하고 정의를 하지 않는 형식을 불완전한 형식이라고 하며, 구현부에 작성하면 된다.

#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>

struct Widget::Impl 
{
    std::string name;
    std:;vector<double> data;
    Gadget g1, g2, g3;
};

Widget::Widget()
: pImpl(new Impl)
{}

Widget::~Widget()
{ delete pImpl; }

여기서 pImpl을 보면 unique_ptr을 사용하기 적합한 상황임을 알 수 있지만, unique_ptr을 사용하게 되면 컴파일이 되지 않는다.

왜냐하면 컴파일러가 자동으로 pImpl의 소멸자를 호출하는데 Impl은 불완전한 형식이기 때문이다.

해결법은 Impl을 완전한 형식으로 만들어주면 되는데, 컴파일러는 타입의 정의를 보게 되면 그 타입을 완전한 타입으로 간주한다. 따라서 컴파일러가 소스파일의 Widget소멸자(unique_ptr 데이터 멤버를 파괴하는 코드를 작성하는 곳)를 보게 한다면 클라이언트 코드가 문제없이 컴파일된다.

struct Widget::Impl
{
    std::string name;
    std:;vector<double> data;
    Gadget g1, g2, g3;
};

Widget::Widget()
: pImpl(std::make_unique<Impl>())
{}

Widget::~Widget() = default;

그리고 이 경우 소멸자를 선언하면 컴파일러는 이동 연산들을 작성하지 않아 이동 연산들을 지원하려면 직접 선언해야 하고, 소멸자와 마찬가지로 정의가 구현 파일에 있어야 한다.

복사 연산도 마찬가지이다. 왜냐하면 unique_ptr 같은 이동 전용 타입이 있는 클래스에 대해서는 컴파일러가 복사 연산들을 작성해주지 않으며, 작성한다고 해도 작성된 함수들은 unique_ptr 자체만 복사하는 얕은 복사를 수행하기 때문이다.

// "widget.h"
class Widget
{
public:
    Widget(void);
    ~Widget(void);
    
    // 선언만 하고 정의는 하지 않는다.
    // 이동 연산
    Widget(Widget&& rhs);
    Widget& operator=(Widget&& rhs);
    
    // 복사 연산
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs);
    ...
    
private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

// "widget.cpp"
#include <string>
#include "widget.h"
...

struct Widget::Impl { ... };
 
Widget::Widget(void)
: pImpl(std::make_unique<Impl>())
{}

// 소멸자
Widget::~Widget(void) = default;

// 이동 연산자
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;

// 복사 생성자 
Widget::Widget(const Widget& rhs)
: pImpl(nullptr)
{ if (rhs.pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl); }
 
// 복사 대입 연산자
Widget& Widget::operator=(const Widget& rhs)
{
    if (!rhs.pImpl) pImpl.reset();
    else if (!pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl);
    else *pImpl = *rhs.pImpl;
    
    return *this;
}

 

위의 내용은 unique_ptr에서만 적용되고 shared_ptr에서는 적용되지 않는다.

왜냐하면 shared_ptr은 삭제자의 타입이 스마트 포인터 타입의 일부가 아니라 실행 시점 자료구조가 더 커지고 실행 코드가 다소 더 느려지지만, 컴파일러가 작성한 특수 멤버 함수들이 쓰이는 시점에서 피지칭 타입들이 완전한 타입 이어야 한다는 요구조건이 사라지기 때문이다.

728x90
반응형