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)には使わない。生成頻度が高いオブジェクトでヒープ確保がボトルネックになりうる。