お茶漬けびより

"あなたに教わったことを、噛んでいるのですよ" 五等分の花嫁 7巻 「最後の試験が五月の場合」より

簡単に Pimpl を使ってみる

名前は知っていたけど、使ったことがなかったので、試しに使ってみた。

Pimpl とは

にきび(pimple)ではありません。Pointer to Implementation (実装へのポインタ)を略して、Pimpl です。
このテクニックによって、プライベートな詳細を開示せずに済みます。つまりインタフェースと実装を切り離す方法です。

これは、実際にコードを見たほうが早いと思うので、先にコードを出します。

実装

とりあえず以下のようなコードを書いてみました。これは、まだ Pimpl を使っていません。

// Widget.h
#pragma once
#include <stdint.h>

class Widget
{
public:
    Widget();
    ~Widget();

    int32_t getNum();
private:
    int32_t num_ = 32;
};

// Widget.cpp
#include "Widget.h"
#include <iostream>

Widget::Widget()
{
    std::cout << "Constructor" << std::endl;
}

Widget::~Widget()
{
    std::cout << "Destructor" << std::endl;
}

int32_t Widget::getNum()
{
    return num_;
}

Widget::getNum() を呼ぶことで、内部の num のデータを取れるクラスです。このクラスを使う側(ユーザ)は、データの取り方が分かればいいので、変数 num があることを知らせる必要はありません。なので、これを実装ファイル(Widget.cpp)に入れてやります。

// Widget.h
#pragma once
#include <stdint.h>

class Widget
{
public:
    Widget();
    ~Widget();

    int32_t getNum();
private:
    class Impl;
    Impl* impl_;
};

// Widget.cpp
#include "Widget.h"
#include <iostream>

class Widget::Impl
{
public:
    Impl() { std::cout << "Impl Constructor" << std::endl; }
    ~Impl() { std::cout << "Impl Destructor" << std::endl; }

    int32_t num = 32;
};

Widget::Widget()
    : impl_(new Impl())
{
    std::cout << "Constructor" << std::endl;
}


Widget::~Widget()
{
    std::cout << "Destructor" << std::endl;
    delete impl_;
    impl_ = nullptr;
}

int32_t Widget::getNum()
{
    return impl_->num;
}

まずヘッダから見ていきます。private にメンバ変数 num がありましたが、それが消えて、代わりに クラス Impl の前方宣言とそのクラス Impl のポインタを追加しました。

次に実装側ですが、初めに クラス Imple の定義を行っています。その中にさっきは、Widget.h にあったメンバ変数が Widget::Impl に移動しています。

Widget のコンストラクタの初期化子リストに impl_ が追加され、デストラクタでは、impl_ の開放を行っています。最後に、Widget::getNum() は、impl_のメンバ変数 num をアクセスして、値を返すように変更されています。

このように、詳細を実装ファイル内に隠してしまうのが、Pimpl です。

Pimpl を実装するときの注意

Pimpl を書くときに、いくつか注意することがあります(C++ に慣れている方は大したことではないですが)。

まず、Pimpl となる変数(今回だと impl_)の型は、ポインタ(Impl*)でないといけません。当たり前ですが、ヘッダ側は、Impl の実装内容を知らないので、Impl のオブジェクトを持てません。

Impl の詳細を書くときは、必ず Impl を利用する処理よりも先に書くようにします。今回だと Widget::Widget() よりも先に書く必要があります。これも Impl の詳細を知らないと使えないためです。

最後に、デストラクタでは必ず Impl の変数を開放するようにします。ポインタなので、当然ですね。

Pimpl をスマートポインタにする

Pimpl をスマートポインタに変更することで、デストラクタ内で Pimpl を開放処理を書かなくて済みます。例えば以下のようにします。

std::unique_ptr<Impl> impl_;

デメリット

Pimpl を使うことでいくつかデメリットがあります。

  • コンストラクタでオブジェクトの割当、デストラクタでオブジェクトを破棄する必要がある
  • すべてのプライベートメンバは、Pimpl を通じてアクセスしないといけない
  • Pimpl をプライベートにすると、Pimpl を持つクラス(Widget)しか Pimpl のメンバにアクセスできない
  • プライベート仮想メソッドを作れない。オーバライドする場合、パブリックにして、派生クラスがオーバライドできるようにしないといけない
  • Pimpl(impl_) から、それを持つクラス(Widget)のパブリックなメンバ変数にアクセスするには、Pimpl にそのクラスへのポインタを追加するか、そのメソッドにクラスを渡す必要がある
  • const メソッド内で Pimpl(impl_)のメンバ変数を変更してもコンパイラは検知することができない

いくつかデメリットはありますが、実装する側のデメリットであり、利用者に対してのデメリットはないので、使わない理由にはならないでしょう。

メリット

次は、メリットです。

  • ユーザ側が見ることになるヘッダファイルの内容がすっきりする。
  • 実装の詳細をユーザ側に見られることがなくなり、不正な方法でアクセスすることもできなくなる。
  • ヘッダのインクルードファイルが減るので、結合度が減る。
  • コンパイル時間が高速化される
  • バイナリ互換性が向上する
  • Pimpl は要求時に作ることもできるので、ネットワーク接続など制限のあるリソースやコストの高いリソースの割当のときに役に立つ。

などなどあります。詳細は、参考にした本を見てください(最後に載せています)

コピーセマンティクス

Pimpl を持つクラスをコピーした場合、当然 Pimpl もコピーされる。この状態で、コピー元か、コピー先のオブジェクトを開放したとき、もう片方のオブジェクトが Pimpl にアクセスすると Pimpl はすでに開放されているため、エラーが発生します。

これを防ぐためには、3つの方法があります。

  • コピーコンストラクタ、代入演算子delete(暗黙定義の禁止) する
  • コピーコンストラクタ、代入演算子を明示的に定義する
  • スマートポインタを利用する

個人的には、スマートポインタが楽だと思います。コピーを禁止したい場合は、std::unique_ptr, コピーを許可するなら std::shared_ptr でいいと思います。 スマートポインタを使えば、コピーセマンティクスを書く必要もなくなります。

おわり

Pimpl は、有効に使えると強力な反面、注意する点が多い方法だと感じました。デメリットは小さいので、積極的に使っていきたいですが、プライベートをなんでもかんでも Pimpl に実装するのも違うと思うので、よく考えて使いたいと思います。

Pimpl の書き方をメインに書いたので、どう活用するかは分かりにくかったかもしれません。そういった内容は、参考にした本に載っているのでそちらを参照するといいでしょう。

今回のコードは、以下に上げています。

github.com

参考にした資料です。

C++のためのAPIデザイン

C++のためのAPIデザイン