콘텐츠로 이동

C++ Pimpl 이디엄 — 컴파일 방화벽

Pimpl(Pointer to Implementation, “d-pointer”라고도 함)은 클래스의 구현 세부사항을 헤더에서 숨기는 이디엄입니다. 구현이 바뀌어도 헤더는 그대로이므로 재컴파일 전파를 차단하고 ABI를 안정화합니다.


// 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 완전 타입 보임

소유권만 이동하고 복사가 필요 없는 경우:

Widget.h
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_; // 복사 시 참조 카운트만 증가
};

항목장점단점
컴파일 시간재컴파일 전파 차단소멸자 .cpp 정의 필요
ABI 안정성구현 변경 시 바이너리 호환간접 참조 오버헤드
캡슐화private 멤버 완전 은닉인라인 최적화 제한
헤더 크기의존성 헤더 숨김힙 할당 추가

Pimpl 사용:

  • 라이브러리 공개 헤더 (ABI 안정성 필수)
  • 무거운 OS/플랫폼 헤더를 감춰야 할 때
  • 컴파일 시간이 병목인 대형 프로젝트

Pimpl 불필요:

  • 내부 구현 클래스
  • 성능 임계 경로의 소형 클래스
  • 템플릿 클래스 (헤더에 구현 필수)

Pimpl의 핵심은 unique_ptr<Impl>을 헤더에 선언하고 소멸자·이동 연산자를 .cpp에서 = default로 정의하는 것입니다. 구현이 얼마나 바뀌어도 헤더 포함자는 재컴파일되지 않으며, 라이브러리의 ABI가 안정적으로 유지됩니다.