TopC++ 入門 › 応用 › PImpl

PImpl イディオム — ヘッダの依存を断ち切る

「ヘッダを直せば全プロジェクトが再ビルド」。大きな C++ プロジェクトではこれが辛い。PImpl (Pointer to Implementation) は実装をヘッダから隔離し、コンパイル時間を劇的に減らします。

1. 何が問題だったのか

C++ のヘッダにクラス定義を書くと、メンバのサイズと型が全部公開される。他のヘッダが間接 include された瞬間、クライアントコードまで再コンパイル対象。

widget.h重い
#include <huge_gui_lib.h> // 5000 行 include #include <db/connection.h> // 2000 行 #include <map> class Widget { HugeGui gui; // ← サイズ必要 DbConnection conn; // ← サイズ必要 std::map<int,std::string> cache; public: void show(); };
widget.h (pimpl)軽い
#include <memory> // それだけ class Widget { struct Impl; // 前方宣言のみ std::unique_ptr<Impl> p; // ポインタならサイズ不要 public: Widget(); ~Widget(); void show(); };

2. cpp 側にすべてを閉じ込める

widget.cpp
#include "widget.h" #include <huge_gui_lib.h> #include <db/connection.h> #include <map> struct Widget::Impl { HugeGui gui; DbConnection conn; std::map<int,std::string> cache; void show_impl() { gui.render(); } }; Widget::Widget() : p(std::make_unique<Impl>()) {} Widget::~Widget() = default; // ← ここに書くことが重要(Impl 完全型が必要) void Widget::show() { p->show_impl(); }
デストラクタは必ず cpp 側で定義。ヘッダで = default にすると、Impl が不完全型の時に unique_ptr のデストラクタがインスタンス化できずコンパイルエラー。

3. メリットとデメリット

メリットデメリット
ヘッダ依存が激減 → コンパイル時間短縮ポインタ一段経由でキャッシュ効率低下
ABI が安定(実装変更で再ビルド不要)ヒープ確保コスト(生成のたびに new)
実装詳細をカプセル化できるコードが cpp と h に分散して追いにくい
クロスコンパイラの互換性向上小さいクラスには過剰

4. コピー/ムーブの扱い

unique_ptr はコピー不可。コピー可能にしたいなら手書きで Impl を複製する必要があります。

copy_support.cpp
Widget::Widget(const Widget& o) : p(std::make_unique<Impl>(*o.p)) {} Widget& Widget::operator=(const Widget& o) { *p = *o.p; return *this; } // move は = default で OK (cpp 側で)

5. いつ使うべきか

  • ライブラリ API: 利用者のビルド時間を守るため(Qt はこの方式)
  • 大規模モノレポ: ヘッダ変更で数千ファイルが再コンパイルする場合
  • プラグイン API: ABI 安定が必要なとき
  • OS 依存の実装を隠すとき: 実装がプラットフォームで分かれるクラス
小さな値クラス(Point, Color)には使わない。生成頻度が高いオブジェクトでヒープ確保がボトルネックになりうる。