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

Flat Leon Works

アプリやゲームを作ってます。

【C++ イディオム】Pimplイディオムを使って真のprivateを実現する

C++ 技術

privateが見えちゃってる問題

C++のクラスにはメンバにprivate指定ができます。privateメンバは非公開となり外部からはアクセスできなくなります。このprivateメンバは内部の実装のために利用するものですが、少し問題があります。その問題とは非公開といいつつ外部からは見えてしまっていることです。

#include "B.h" // メンバ変数としてBを使っているのでインクルードが必要
class A
{
public:
    void Action( void );
private:
    // アクセスはできないけど見えてしまっている
    void ActionImpl( void ){ m_B.Action(); }
    B m_B;
};

これの何が問題なのかというと、

  • "B.h"をインクルードする必要が発生している
  • 外部に不要な情報が公開されてしまっている

ヘッダで他のヘッダをインクルードすることは、【C++】クラスの前方宣言いろいろ - Flat Leon Worksでも記述したように、複雑なインクルードによるコンパイルエラーの発生やコンパイル時間の増大という問題があり、なるべく避けるべきことです。

Pimplイディオムとは

Pimplイディオムは、先ほどのようなprivateの問題を解決することができます。

//--------------------
// A.h
//--------------------
class A
{
public:
    A();
    ~A();
    void Action( void );
private:
    class APrivate* m_pPrivate; // プライベートクラスの前方宣言+ポインタ変数の宣言
};

//--------------------
// A.cpp
//--------------------
#include "A.h"
#include "B.h"

// プライベートクラス
class APrivate
{
public:
    void ActionImpl( void ){ m_B.Action(); }
    B m_B;
};

A::A() : m_pPrivate( new APrivate ){}
A::~A(){ delete m_pPrivate; }

void A::Action( void ){ m_pPrivate->ActionImpl(); }

APrivateクラスが内部実装のためのメンバ変数やメンバ関数を持つことになります。ポイントはクラスAではAPrivateクラスの前方宣言とポインタ変数の宣言しかしていないことです。これにより内部実装を完全に隠すことができます。

これをPimplイディオムといいます。名前の由来はPはポインタ、implは実装(implmentation)で、実装クラス(プライベートクラス)へのポインタを持つということだと思います。それに加え英語のpimple(にきび)をもじったものらしいです。

Pimplイディオムのメリット

  • ヘッダでの他のヘッダファイルのインクルードを減らすことができる
  • コンパイル時間を削減できる
  • クラスのヘッダが見やすくなる

Pimplイディオムのデメリット

  • newが発生する
  • メンバ関数のinline実装することができない
  • ちょっと手間

プライベートクラスを一部に公開する

フレンド宣言やprotectedによりプライベートクラスへのポインタを一部にのみ公開する場合、そのままではプライベートクラスのメンバにアクセスすることはできません。この場合、プライベートクラス用のヘッダファイル(とソースファイル)を用意することで、プライベートクラスを一部に公開することができます。

//--------------------
// A.h
//--------------------
class A
{
public:
    A();
    ~A();
    void Action( void );
protected: // 派生クラスからはアクセス可能
    class APrivate* m_pPrivate;
};

//--------------------
// APrivate.h
//--------------------
#include "B.h"

// プライベートクラス
class APrivate
{
public:
    void ActionImpl( void ){ m_B.Action(); }
    B m_B;
};

//--------------------
// A.cpp
//--------------------
#include "A.h"
#include "APrivate.h"

A::A() : m_pPrivate( new APrivate ){}
A::~A(){ delete m_pPrivate; }

void A::Action( void ){ m_pPrivate->ActionImpl(); }

//--------------------
// AA.h
//--------------------
#include "A.h"

class AA : public A
{
public:
    void Hoge( void );
};

//--------------------
// AA.cpp
//--------------------
#include "AA.h"
#include "APrivate.h"

void AA::Hoge( void )
{
    m_pPrivate->ActionImpl();
}

Pimplイディオムを使う場面

  • ヘッダでの他のヘッダファイルのインクルードをしたくない場合
  • クラス規模が大きくなりそうな場合

逆にPimplイディオムを使わない場面は、

  • 小規模なクラスの場合
  • パフォーマンスが気になる場合

全部のクラスでPimplイディオムを使う必要はありません。また、Pimplイディオムを使うとしてもすべての実装をプライベートクラスで行う必要はありません。都度使い分けましょう。

Pimplイディオムの注意点

C++では明示的に定義しない場合、必要に応じて自動でコピーコンストラクタや代入演算子が作成されます。この自動で作成されるコピーコンストラクタや代入演算子は単純にすべてのメンバをコピー(代入)する挙動になっています。pimplイディオムを使用しているときに問題になるのが、プライベートクラスへのポインタが単純に代入でコピーされてしまうことです。ポインタがコピーされるだけなのでコピー元とコピー先は実質同じデータを指すことになってしまします。コピー禁止処理を行うか、ポインタの中身を代入するようなコピーコンストラクタと代入演算子を自分で作成しましょう。

まとめ

Pimplイディオムはクラスの前方宣言と合わせてC++の重要なテクニックの一つです。是非活用していきましょう。

参考リンク