[Effective Modern C++] 22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라
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은 삭제자의 타입이 스마트 포인터 타입의 일부가 아니라 실행 시점 자료구조가 더 커지고 실행 코드가 다소 더 느려지지만, 컴파일러가 작성한 특수 멤버 함수들이 쓰이는 시점에서 피지칭 타입들이 완전한 타입 이어야 한다는 요구조건이 사라지기 때문이다.
'Books > Effective Modern C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
2022.10.23 -
[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
2022.09.24 -
[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
2022.09.18 -
[Effective Modern C++] 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
[Effective Modern C++] 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
2022.09.18
댓글을 사용할 수 없습니다.