디자인 패턴 PImpl(Pointer to Implementation), 또한 Opaque Pointer로 알려진 이 패턴은 C++에서 클래스의 인터페이스와 구현을 분리하는 수단으로 등장했습니다. 이는 바이너리 인터페이스의 호환성(ABI)을 보장하고, 컴파일 시간을 줄이며, 클래스 사용자에게 구현 세부 사항을 숨기는 것이 특히 중요합니다.
문제의 역사.
많은 C++ 프로젝트에서 클래스의 공개 인터페이스를 변경하지 않고도 구현을 수정해야 하는 경우가 발생합니다. 문제는 헤더 파일에서의 변경이 모든 의존 모듈의 재컴파일을 요구하기 때문에, 이는 큰 코드 베이스에서 매우 비용이 많이 들 수 있습니다. PImpl는 재컴파일을 최소화하고 더 나은 캡슐화를 제공합니다.
문제점.
헤더 파일에서 비공식 멤버를 정의하는 일반적인 방법은 컴파일 시 이러한 모든 멤버를 아는 것을 요구합니다. 이를 확장하거나 변경하면 헤더를 포함하는 모든 파일을 재컴파일해야 합니다. 또한 이는 클라이언트의 구현/구조 세부 사항을 드러내어 보안 및 아키텍처 무결성에 부정적인 영향을 미칠 수 있습니다.
해결책.
PImpl는 정의된 cpp 파일에서 포워드 선언된 구조체 구현(Impl struct/class)에 대한 포인터를 사용함으로써 구현을 숨깁니다. 이를 통해 인터페이스를 건드리지 않고도 구현을 변경할 수 있습니다.
코드 예제:
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // opaque pointer }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // 내부 비밀 Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
주요 특징:
PImpl에서 원시 포인터 대신 std::unique_ptr를 사용할 수 있나요?
네, 현대적이고 안전한 접근 방식은 std::unique_ptr(또는 소유권 공유가 필요한 경우 std::shared_ptr)를 사용하는 것입니다. 이는 메모리 관리를 정확히 하고 원시 포인터에 대해 명시적으로 소멸자/복사 연산자를 작성할 필요가 없게 해줍니다:
private: std::unique_ptr<Impl> pimpl;
PImpl 클래스를 이동 가능하지만 복사할 수 없도록 만들 수 있나요?
네, 이동 생성자/연산자를 제공하고 복사 생성자를 삭제하면 가능합니다. 예:
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
PImpl 사용 시 성능 오버헤드가 발생하나요?
네, 포인터 역참조와 추가적인 동적 메모리 할당(힙 할당)으로 인해 발생합니다. 성능이 крит이 중요한 구조에는 상당한 단점이 될 수 있습니다.
큰 회사가 모든 클래스에 대해 PImpl를 도입했는데, 단순한 데이터 구조에도 적용되었습니다. 이는 지속적인 포인터 역참조로 인해 단순 작업의 속도를 크게 떨어뜨렸습니다.
장점:
단점:
오래 지속되는 사용자 인터페이스 라이브러리 프로젝트에서 PImpl는 자주 변경되는 내부를 가진 복잡한 위젯에만 적용되어 외부 클라이언트를 위한 안정적인 ABI를 유지했습니다.
장점:
단점: