C++ Pimpl 이디엄 — 컴파일 방화벽
Pimpl(Pointer to Implementation, “d-pointer”라고도 함)은 클래스의 구현 세부사항을 헤더에서 숨기는 이디엄입니다. 구현이 바뀌어도 헤더는 그대로이므로 재컴파일 전파를 차단하고 ABI를 안정화합니다.
1. 기본 구조
섹션 제목: “1. 기본 구조”// Widget.h — 헤더: 구현 세부사항 없음#pragma once#include <memory>#include <string>
class Widget {public: Widget(); ~Widget(); Widget(Widget&&) noexcept; Widget& operator=(Widget&&) noexcept;
// 복사는 선택적 Widget(const Widget&); Widget& operator=(const Widget&);
void setName(std::string name); std::string getName() const; void render();
private: struct Impl; // 전방 선언만 std::unique_ptr<Impl> pImpl_; // 포인터만 헤더에};// Widget.cpp — 구현: 헤더를 include하는 쪽은 이 코드를 볼 수 없음#include "Widget.h"#include "HeavyDependency.h" // 헤더 사용자에게 노출되지 않음#include <iostream>
struct Widget::Impl { std::string name; HeavyDependency dep; // 무거운 의존성 숨김 int internalState = 0;
void render() { std::cout << "Rendering: " << name << '\n'; dep.doWork(); }};
Widget::Widget() : pImpl_(std::make_unique<Impl>()) {}Widget::~Widget() = default; // unique_ptr 소멸자가 Impl을 안전하게 해제
Widget::Widget(Widget&&) noexcept = default;Widget& Widget::operator=(Widget&&) noexcept = default;
Widget::Widget(const Widget& o) : pImpl_(std::make_unique<Impl>(*o.pImpl_)) {}
Widget& Widget::operator=(const Widget& o) { *pImpl_ = *o.pImpl_; return *this;}
void Widget::setName(std::string name) { pImpl_->name = std::move(name); }std::string Widget::getName() const { return pImpl_->name; }void Widget::render() { pImpl_->render(); }2. 소멸자를 .cpp에 정의해야 하는 이유
섹션 제목: “2. 소멸자를 .cpp에 정의해야 하는 이유”unique_ptr<Impl>의 소멸자는 Impl의 완전한 타입이 필요합니다. 헤더에서 ~Widget() = default를 쓰면 컴파일 오류가 발생하므로 반드시 .cpp에서 정의합니다.
// 헤더에 이렇게 쓰면 오류:// Widget::~Widget() = default; // ❌ Impl이 불완전 타입
// .cpp에서:Widget::~Widget() = default; // ✓ Impl 완전 타입 보임3. 이동 전용 Pimpl
섹션 제목: “3. 이동 전용 Pimpl”소유권만 이동하고 복사가 필요 없는 경우:
class Widget {public: Widget(); ~Widget(); Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default;
// 복사 삭제 Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
void process();private: struct Impl; std::unique_ptr<Impl> pImpl_;};4. shared_ptr 기반 (공유 가능 Pimpl)
섹션 제목: “4. shared_ptr 기반 (공유 가능 Pimpl)”// 복사 시 구현을 공유 (COW 패턴 구현 가능)class Config {public: Config(); void set(std::string key, std::string val); std::string get(const std::string& key) const;private: struct Impl; std::shared_ptr<Impl> pImpl_; // 복사 시 참조 카운트만 증가};5. Pimpl 장단점
섹션 제목: “5. Pimpl 장단점”| 항목 | 장점 | 단점 |
|---|---|---|
| 컴파일 시간 | 재컴파일 전파 차단 | 소멸자 .cpp 정의 필요 |
| ABI 안정성 | 구현 변경 시 바이너리 호환 | 간접 참조 오버헤드 |
| 캡슐화 | private 멤버 완전 은닉 | 인라인 최적화 제한 |
| 헤더 크기 | 의존성 헤더 숨김 | 힙 할당 추가 |
6. 실전 사용 기준
섹션 제목: “6. 실전 사용 기준”Pimpl 사용:
- 라이브러리 공개 헤더 (ABI 안정성 필수)
- 무거운 OS/플랫폼 헤더를 감춰야 할 때
- 컴파일 시간이 병목인 대형 프로젝트
Pimpl 불필요:
- 내부 구현 클래스
- 성능 임계 경로의 소형 클래스
- 템플릿 클래스 (헤더에 구현 필수)
Pimpl의 핵심은 unique_ptr<Impl>을 헤더에 선언하고 소멸자·이동 연산자를 .cpp에서 = default로 정의하는 것입니다. 구현이 얼마나 바뀌어도 헤더 포함자는 재컴파일되지 않으며, 라이브러리의 ABI가 안정적으로 유지됩니다.