-
항목22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라Effective Modern C++ 2022. 8. 16. 17:10
Pimpl 관용구는 너무 긴 빌드 시간을 줄이는 방법이다. 기본적인 방식은 클래스의 자료 멤버들을 구현 클래스를 가리키는 포인터로 대체하고, 자료 멤버들을 구현 클래스로 옮기고, 포인터를 통해서 그 자료 멤버들에 간접적으로 접근하는 기법이다.
class Widget { // 헤더 "Widget.h" 안에서 public: Widget(); ~Widget(); // 선언만 해둔다 Widget(Widget&& rhs); // 이동 연산 Widget& operator=(Widget&& rhs); // 선언만 해둔다 Widget(const Widget& rhs); // 복사 연산 Widget& operator=(const Widget& rhs); // 선언만 해둔다 ... private: struct Impl; std::unique_ptr<Impl> pImpl; };#include "widget.h" // "widget.cpp" 파일 안에서 #include "gadget.h" #include <string> #include <vector> struct Widget::Impl{ std::string name; std::vector<double> data; Gadget g1,g2,g3; }; Widget::Widget() : pImpl(std::make_unique<Impl>()) // 구성원 초기화 {} Widget::~Widget() // ~Widget의 정의 {} //Widget::~Widget() = default; // 기본 기능으로 충분하다 강조함 // 이동 연산들 정의 Widget::Widget(Widget&& rhs) = default; Widget& Widget::operator=(Widget&& rhs) = default; // Gadget이 복사가 가능한 형식이라면 // 복사 연산들 정의 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; }스마트 포인터를 사용했기 때문에 직접 소멸자에서 포인터를 삭제해줄 필요가 없다. 하지만 소멸자가 없으면 기본 소멸자가 호출되고 그러면 헤더 파일에서 Widget::Impl은 불완전 형식(자동 생성된 소멸자는 암묵적으로 inline이기 때문이다.)이기 때문에 오류가 발생한다. 그래서 소스코드(구현 파일)에서 Widget::Impl의 정의 이후에 소멸자의 정의를 생성하면 된다.
또한 소멸자를 정의했기 때문에 이동 연산들은 자동으로 선언되지 않는다. 그래서 역시 헤더에서는 선언만하고 소스파일에서 정의한다.
Gadget 또한 복사가능한 형식이라면 3법칙에 의해 (소멸자 또는 이동 연산이 있으니) 복사연산도 직접 정의해줘야 한다. 작성된다고 해도, 작성된 함수들은 std::unique_ptr 자체만 복사하는 얕은 복사를 수행한다. 우리가 원하는 것은 포인터가 가리키는 대상까지 복하는 것, 즉 깊은 복사이다.
여기까지는 std::unique_ptr을 사용할 때 이야기였다. std::shared_ptr을 사용할 때는 과연 어떨까?
class Widget { // 헤더 "Widget.h" 안에서 public: Widget(); ... // 소멸자나 이동 연산들의 선언이 전혀 없음 private: struct Impl; std::shared_ptr<Impl> pImpl;std::unique_ptr에서 삭제자의 형식은 해당 스마트 포인터 형식의 일부이고, std::shared_ptr은 아니다. 그래서 후자는 컴파일러가 작성한 특수 멤버 함수들이 쓰이는 시점에서 피지칭 형식들이 완전한 형식이어야 한다는 요구조건이 사라진다.
하지만 std::unique_ptr은 더 작은 실행시점 자료구조와 더 빠른 실행시점 코드를 만들어 낼 수 있다는 장점이 있다.
'Effective Modern C++' 카테고리의 다른 글
항목24. 보편 참조와 오른값 참조를 구별하라 (0) 2022.08.27 항목23. std::move와 std::forward를 숙지하라 (0) 2022.08.20 항목21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라 (0) 2022.08.11 항목20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr을 사용하라 (0) 2022.08.11 항목19. 소유권 공유 자원의 관리에는 std__shared_ptr를 사용하라 (0) 2022.08.05