読者です 読者をやめる 読者になる 読者になる

Pimplイディオム

読書会で「C++は循環参照の記述が困難なところが・・・」という話題があったけど、その場はPimplイディオムという言葉のみを紹介しておいた。
ということで補足を。


まず、問題となるコードはこんな感じ。

// hoge.h
#pragma once

#include "piyo.h"

class hoge
{
    piyo p;
};
// piyo.h
#pragma once

#include "hoge.h"

class piyo
{
    hoge h;
};

このコードはどうがんばってもコンパイルできない。
なぜなら、hogeをコンパイルする為にはpiyoが必要で、piyoをコンパイルする為にはhogeが必要だから。


これを、Pimplイディオムを使用して回避するとこんな感じになる。

// hoge.h
#pragma once

class hoge
{
    // hogeクラスのprivateメンバ部分を内部に持つ構造体の先行宣言
    struct impl;
    // 上の構造体をポインタとして持つ
    impl* pimpl;
public:
    hoge();
    ~hoge();
};
// piyo.h
#pragma once

class piyo
{
    struct impl;
    impl* pimpl;
public:
    piyo();
    ~piyo();
};
// hoge.cpp
#include "hoge.h"

#include "piyo.h"

// hoge::implの定義
struct hoge::impl
{
    // ここにhogeクラスのprivateメンバを記述する
    piyo p;
};

// 初期化リストでpimplを初期化
hoge::hoge() : pimpl(new impl) {}

// デストラクタで破棄
hoge::~hoge() { delete pimpl; }
// piyo.cpp
#include "piyo.h"

#include "hoge.h"

struct piyo::impl
{
    hoge h;
};

piyo::piyo() : pimpl(new impl) {}

piyo::~piyo() { delete pimpl; }

このようにすることで、hoge.hやpiyo.hから#includeがなくなり、.cppに移動している。


Pimplイディオムの最大の欠点は、自分のprivateなメンバにアクセスする為に、いちいちpimpl->と記述しなければならないところかな。
あと、templateクラスにはそのままでは適用できない点*1

Pimplイディオムは本来、ファイル間の依存関係を減らして、あるファイルに変更があった場合に再コンパイルが必要なファイル数および時間をできる限り少なくする為のイディオムだけど、このように循環参照も解決してくれる。
さらに、ヘッダから余計な情報がなくなり*2、すっきりと見通しが良くなるのも利点だと思う。


また、Strategyパターンをtemplateを使って静的に実装する例は良く知られているけど、Pimplイディオムを使って、「いくつか実装ファイルを作成しておき、コンパイル時(またはリンク時)にどれをコンパイル/リンクするかを選択する」ことでも実現できる。


Pimplイディオムの実装方法としては、生のポインタを使用せずにboost::shared_ptr*3を使う方法も考えられる*4
この方法を採用した場合、デストラクタでdeleteする必要がなくなる。

*1:そもそもtemplateクラスは実装をきれいにヘッダと実装に分割できない

*2:先行宣言とポインタはまけといて下さい

*3:やstd::tr1::shared_ptr

*4:他の使えそうなスマートポインタは、テンプレート型引数に不完全型が渡せないので使えない