Personal tools
You are here: Home ブログ matsuyama pimplイディオムを語る
Document Actions

pimplイディオムを語る

foo.h:

class foo { ... };

bar.h:

class bar { ... };

があるとして

hoge.h:

#include "foo.h"
#include "bar.h"

class hoge {
private:
    foo f;
    bar b;
public:
    hoge();
    foo& get_foo();
    bar& get_bar();
};

hoge.cpp:

#include "hoge.h"

hoge::hoge() {}
foo& hoge::get_foo() { return f; }
bar& hoge::get_bar() { return b; }

と書くより

hoge.h:

#include <memory>

class foo;
class bar;

class hoge {
private:
    struct impl;
    std::auto_ptr<impl> pimpl_;
public:
    foo& get_foo();
    bar& get_bar();
};

hoge.cpp:

#include "foo.h"
#include "bar.h"
#include "hoge.h"

struct hoge::impl {
    foo f;
    bar b;
};

hoge::hoge() : pimpl_(new impl) {}
foo& hoge::get_foo() { return pimpl_->f; }
bar& hoge::get_bar() { return pimpl_->b; }

と書くほうがすばらしいよという話。前者の場合はhoge.hをincludeした時点でfoo.hとbar.hまでincludeされてしまって、おまけにfoo.hやbar.hに変更が加えられてしまった場合にhoge.hはもちろんのことそれをincludeしているヘッダファイルまで変更が波及してしまうことになり、結果としてコンパイルが遅くなる。一方後者の場合はhoge.hの他への依存性はほぼゼロである。前方宣言を使うのは当然のことであるが、ここではそれ以上にpimplイディオムが大活躍している。つまりimplのポインタにfooやbarを格納してしまうことにより、それらのサイズや定義を知る必要がないのだ。ゆえにいくらhoge.hをincludeしているヘッダファイルがあろうともfoo.hやbar.hの変更はそれらに波及しえない。これによって変更に対する耐性が保証され、結果としてコンパイルが速くなる。

しかしpimplイディオムのメリットはそれだけではない。いやpimplの本質上、メリットがコンパイルの高速化だけでとどまること自体がそもそもおかしい。挙げだすと切りがないのだが(要するにstackにおくこととheapにおくこととの差によるメリット)、個人的に非常に興味深いのは例外安全に対する驚くべき能力である。たとえばfoo.hが以下のようなアホなコードだったとしよう。

foo.h:

class foo {
public:
...
foo& operator =(const foo& rhs) {
    throw not_implemented_error(); // how stupid I am!
}
... 
};

このアホクラスを使ってテストコードを書く。

hoge a;
hoge b;
a = b;

このコードをpimplイディオムでないバージョンのhogeでコンパイルすると a = b の部分で例外が発生する。依存したクラスのおかげで自分のクラスまで非例外安全になるなんてなんてすばらしいのでしょう。しかしpimplイディオムの黒魔術にかかればそんな問題も一発。hoge.hとhoge.cppを以下のように改造する。

hoge.h:

#include <memory>

class foo;
class bar;

class hoge {
private:
    struct impl;
    std::auto_ptr<impl> pimpl_;
public:
    foo& get_foo();
    bar& get_bar();
    hoge& operator =(const hoge& rhs); // never throw
};

hoge.cpp:

#include "foo.h"
#include "bar.h"
#include "hoge.h"

struct hoge::impl {
    foo f;
    bar b;
};

hoge::hoge() : pimpl_(new impl) {}
foo& hoge::get_foo() { return pimpl_->f; }
bar& hoge::get_bar() { return pimpl_->b; }
hoge& hoge::operator =(const hoge& rhs) {
    if (this != &rhs) {
        pimpl_ = rhs.pimpl_;
    }
    return *this;
}

これで何回 hoge::operator = しようが決して例外が発生することはない。すばらしすぎる。

その他にも new impl_ するタイミングを調整できたり、 struct impl と本来のクラスに実装をわけて構成をわかりやすくしたり、(時と場合によるが)本体のコピーやswapが断然速いなど、pimplにはたくさんのすばらしいメリットがある。ただもちろんそんなにおいしいことばかりでもなく、heapを使う分のパフォーマンス低下は当然考えられる問題だ。しかしメリットとデメリットを計算したとき、最終的にはやはり符号は+になることだろう。

Category(s)
c++
program
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/pimpl30a430a330aa30e030928a9e308b/tbping

Re:pimplイディオムを語る

Posted by valp at 2008-03-19 00:00
auto_ptrではなくてshared_ptr(boostあるいは最近だとstd::tr1)にしないとoperator =はまずいです。標準のauto_ptrは参照カウントなどではないので、現在の規格に合致した処理系ではpimpl_ = rhs.pimpl_;が思った通りにならないはずです。

Re:pimplイディオムを語る

Posted by M太郎 at 2009-04-24 09:57
>思った通りにならないはずです。
pimpl_ = rhs.pimpl_;

delete pimpl_;
pimple_ = rhs.pimple_;
ってことになってOKじゃないですか?

Re:pimplイディオムを語る

Posted by Anonymous User at 2009-05-15 13:48
おそらく、どちらのauto_ptrが最終的にポインタをdeleteするのか?って問題のことをおっしゃってるのでは無いでしょうか?
auto_ptr a;
{
auto_ptr b;
b = a;
//ここでbがデストラクタされる、ポインタ先を解放する
}
//ここの時点で、aはすでに解放された不正なポインタ指してる。

ってな具合で。

Re:pimplイディオムを語る

Posted by Fumi at 2010-01-02 16:06
>思った通りにならないはずです。
pimpl_ = rhs.pimpl_;

pimpl_.reset(rhs.pimpl_.release());
だから

auto_ptr a(new Hoge());
{
  auto_ptr b;
  b = a; // ここでaはbに所有権を委譲し,aは0ポインタを指す.
  // ここでbのデストラクタが呼び出され,ポインタ先を解放する.
}
// ここの時点で,aは0ポインタ指している.


ってな具合でここの動作に関しては問題ないと思います.

Re:pimplイディオムを語る

Posted by hirose at 2010-01-08 00:29
pimpleイディオムでauto_ptrを使うこと自体が拙いです。
代入してpimpleの所有権が移ってしまうと、元のオブジェクトが持ってたpimpleはどこへ?

boostを使ってよいのなら、pimpleにはboost::scoped_ptr を使うべきです。
#ついでに、pimpleにboost::shared_ptrを使うのも危険です。うっかりしたoperator=を実装すると、複数のオブジェクトで同じpimpleを共有してしまうので。

Re:pimplイディオムを語る

Posted by chanel at 2010-05-27 18:46
nice post .#ついでに、pimpleにboost::shared_ptrを使うのも危険です。うっかりしたoperator=を実装すると、複数のオブジェクトで同じpimpleを共有してしまうので。

Re:pimplイディオムを語る

Posted by valp at 2010-08-30 19:18
ふとしたきっかけで、久々にこのページに出会いました。せっかくなので、自分のコメントに補足を書き足していきます。

pimpl_ = rhs.pimpl_;について、2009-05-15 13:48のAnonymous Userさんやhiroseさんのおっしゃっているのと同じことを指摘するつもりで最初にコメントを書きました。曖昧な記述で混乱を招きましたら、申し訳ございませんでした。ただ、今になって気付いたのですが、実際にはrhsがconstのためコンパイルエラーになりますね(代入元がconstだとauto_ptrはエラーになります)。どちらにせよ、「参照カウントではなく所有権の移動という挙動を持っているためauto_ptrでは問題がある」ということに変わりはありません。

さらに、最初にコメントをしたとき私はまだ知りませんでしたが、auto_ptrを使ってはならないもう1つの理由があります。fooやbarのデストラクタ(根本的にはhoge::implのデストラクタ)が呼ばれないのです(こっちのほうがよっぽど深刻!)。pimpl_のデストラクタで、hoge::impl(不完全型)へのポインタに対しdeleteするのがその原因です。なお、boost::scoped_ptrだとこれを検出してコンパイルエラーにしてくれます。

これの対処は次のいずれかで可能です。
1. boost::shared_ptr / std::shared_ptrを使う: 動的削除子 (dynamic deleter) - 意外と知られていない??boost::shared_ptr の側面 ― Cry's Diary
2. hogeに明示的なデストラクタの定義を与える(boost::scoped_ptr / std::auto_ptrで適用可能): pimplイディオムをscoped_ptrを使って実現するときの注意 ― redboltzの日記
Add comment

You can add a comment by filling out the form below. Plain text formatting.

(Required)
(Required)
(Required)
(Required)
(Required)
This helps us prevent automated spamming.
Captcha Image


Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.