이제는 Widget이 std::string이나 std::vector, Gadget 형식을 언급하지 않으므로, Widget의 클라이언트는 그 형식들의 헤더들을 #include로 포함시킬 필요가 없습니다. 이 덕분에 컴파일이 빨라지며, 또한 그 헤더들에서 뭔가가 바뀌어도 Widget 클라이언트에는 영향이 미치지 않습니다.
선언만 하고 정의는 하지 않은 형식을 불완전 형식(incomplete type)이라고 부르기도 합니다. Widget::Impl이 그런 형식입니다. 불완전 형식을 가리키는 포인터를 선언하는 것은 불완전 형식으로 할 수 있는 몇 안 되는 일 중 하나이고, Pimpl 관용구는 바로 그러한 능력을 활용합니다.
이처럼 불완전 형식을 가리키는 포인터를 하나의 자료 멤버로 선언하는 것이 Pimpl 관용구 적용의 첫 단계입니다. 둘째 단계는 원래의 클래스에서 사용하던 자료 멤버들을 담는 객체를 동적으로 할당, 해제하는 코드를 추가하는 것입니다. 그러한 할당 및 해제 코드는 클래스를 구현하는 소스 코드 파일에 둡니다. Widget이라면 widget.cpp에 두면 될 것입니다.
#include "widget.h" // 구현 파일 "widget.cpp" 안에서
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl { // 전에 Widget에 있던 자료 멤버들을 담은 Widget::Impl의 정의
std::string name;
std::vector<double data;
Gadget g1, g2, g3;
};
Widget::Widget() // 이 Widget 객체를 위한 자료 멤버들을 할당
: pImpl(new Impl)
{}
Widget::~Widget() // 이 객체를 위한 자료 멤버들을 파괴
{ delete pImpl; }
std::string과 std::vector, Gadget의 헤더들에 대한 전반적인 의존성이 아예 사라진 것은 아니라는 점을 확실히 하기 위해, 예제 코드에 해당 #include 지시문들도 표시했습니다. 그러나 중요한 것은, 이 의존성들이 widget.h(Widget 클라이언트가 볼 수 있고 사용하는)에서 widget.cpp(Widget의 구현자만 볼 수 있고 사용하는)로 옮겨졌다는 점입니다. 또한 예제 코드에는 Impl 객체를 동적으로 해제하고 할당하는 코드가 강조되어 있습니다. Widget이 파괴될 때 이 객체도 해제되어야 하므로, Widget에 반드시 소멸자가 필요합니다.
그런데 이는 C++98용 코드이며, 그래서 지난 세기의 악취 나는 유산이 그대로 남아 있습니다. 이 코드는 생 포인터를 사용한다는 점과 new와 delete를 직접 호출한다는 점에서 너무나 원시적입니다. 이번 장의 핵심은 생 포인터보다 똑똑한 포인터를 선호하라는 것이고, 우리가 원하는 것이 Widget 생성자에서 Widget::Impl 객체를 할당하고 Widget이 파괴될 때 그 객체를 해제하는 것이라는 점을 생각하면, 지금 딱 필요한 수단은 바로 std::unique_ptr(Chapter (18))임이 분명해 보입니다. 다음은 헤더 파일에서 생 포인터 pImpl을 std::unique_ptr로 대체한 결과입니다.
class Widget { // 헤더 "widget.h" 안에서
public:
Widget();
...
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() // Chapter (21)의 조언을 따라, std::make_unique를 이용해서 std::unique_ptr을 만듬
: pImpl(std::make_unique<Impl>())
{}
이제는 Widget 클래스에 소멸자가 없는 점이 눈에 띌 것입니다. 소멸자를 없앤 것은, 딱히 소멸자에 집어넣을 코드가 없기 때문입니다. std::unique_ptr가 파괴될 때, std::unique_ptr는 자신이 가리키는 객체를 자동으로 삭제합니다. 따라서 이 클래스에서 따로 삭제할 것은 없습니다. 이처럼 자원을 프로그래머가 손수 해제할 필요가 없다는 것이 똑똑한 포인터의 매력 중 하나입니다.
이 코드 자체는 잘 컴파일되지만, 다음과 같이 자명한 클라이언트 쪽 용법은 컴파일되지 않습니다.
#include "widget.h"
Widget w; // 오류!
구체적인 오류 메시지는 독자가 사용하는 컴파일러에 따라 다르겠지만, 대부분 불완전한 형식에 sizeof나 delete를 적용하는 것과 관련된 불평이 메시지에 포함되어 있을 것입니다. 그런 연산들은 불완전한 형식으로 할 수 있는 몇 안 되는 일에 포함되지 않습니다.
std::unique_ptr를 이용한 Pimpl 관용구의 이러한 명백한 실패는 (1) std::uniqueA_ptr가 불완전한 형식을 지원하다고 광고된다는 점과 (2) Pimpl 관용구가 std::unique_ptr의 가장 흔한 용도 중 하나라는 점에서 놀라운 일입니다. 다행히 이런 클라이언트 코드가 컴파일되게 만드는 것은 어렵지 않습니다. 문제의 원인을 기본적으로 이해하기만 하면 됩니다.
이 문제는 w가 파괴되는 지점(이를테면 범위에서 벗어나는 지점)에 대해 컴파일러가 작성하는 코드에서 기인합니다. 그 지점에서 w의 소멸자가 호출되는데, std::unique_ptr를 이용하는 Widget 클래스에 따로 소멸자를 선언되어 있지는 않습니다(소멸자에서 따로 해야 할 일이 없기 때문). 이 경우, 컴파일러가 생성하는 특수 멤버 함수에 관한 통상적인 규칙들(Chapter (17) 참고)에 의해 컴파일러가 생성하는 특수 멤버 함수에 관한 통상적인 규칙들(Chapter (17) 참고)에 의해 컴파일러가 대신 소멸자를 작성해 줍니다. 컴파일러는 그 소멸자 안에 Widget의 자료 멤버 pImpl의 소멸자를 호출하는 코드를 삽입합니다. pImpl은 std::unique_ptr<Widget::pImpl>, 즉 기본 삭제자를 사용하는 std::unique_ptr이고, 그 기본 삭제자는 std::unique_ptr안에 있는 생 포인터에 대해 delete를 적용하는 함수입니다. 그런데 대부분의 표준 라이브러리 구현들에서 그 삭제자 함수는 delete를 적용하기 전에, 혹시 생 포인터가 불완전한 형식을 가리키지는 않는지를 C++11의 static_assert를 이용해서 점검합니다. 컴파일러가 Widget 객체 w의 파괴를 위한 코드를 산출하는 과정에서 일반적으로 그 static_assert가 참이 아닌 것으로 판정되며, 그러면 앞에서 언급한 오류 메시지가 나타납니다. 그 메시지는 w가 파괴되는 지점과 연관되는데, 이는 컴파일러가 자동 작성하는 다른 모든 특수 멤버 함수처럼 Widget의 소멸자는 암묵적으로 inline이기 때문입니다. 오류 메시지 자체는 소스 코드에서 w가 생성되는 행 번호를 가리키는 경우가 많습니다. 이후의 암묵적 파괴로 이어지는 객체를 명시적으로 생성하는 부분이 바로 그 행이기 때문입니다.
std::unique_ptr<Widget::Impl>을 파괴하는 코드가 만들어지는 지점에서 Widget::Impl이 완전한 형식이 되게 하면 문제가 바로 해결됩니다.