• ベストアンサー

shared_ptrからpimplのデストラクタ呼び出し

pimplイディオムの勉強をするために以下のようなプログラムを作りました。 //MySharedPtr.h template<typename tnT> class MySharedPtr {  tnT *mPtr; public:  MySharedPtr(tnT *ptr) : mPtr(ptr) {}  ~MySharedPtr() { delete mPtr; } //(1) }; //Foo.h class Foo {  struct stImpl;  MySharedPtr<stImpl> mImpl; //(2) public:  Foo(); }; //Foo.cpp Foo::stImpl {  ~stImpl() { cout << "xxx" << endl; } //(3) }; Foo::Foo() : mImpl(new stImpl) {} //main.cpp int main() {  Foo foo; } これをコンパイルすると、(1)の所でtnTのデストラクタが見つからない旨のワーニングが出て、mainを実行するとstImplのデストラクタが呼ばれずに(3)の出力は出ません。ただ、(2)をboost::shard_ptrに変えるとワーニングも出ないし(3)の出力もされます。(2)の箇所でstImplが宣言だけなのは両者とも同じだと思うのですが、なぜboostはワーニングが出ないのでしょうか。また、MySharedPtrでもワーニングを出さないようにすることはできるのでしょうか。もちろん、スマートポインタを実装するよりもboostを使用した方が良いとは思うのですが、何でboostはうまくいくのか不思議に思い、質問させていただきました。

質問者が選んだベストアンサー

  • ベストアンサー
  • 1839cc
  • ベストアンサー率54% (12/22)
回答No.7

> クラスのデストラクタの先頭にはデータメンバのデストラクタを呼び出すコードがコンパイラにより自動的に追加されるとあります > (これは概念を表現しただけのものかもしれませんが、理解する上では問題ないと思います)。 メカニズムの話ですね。 いえ、概念ではなく、実際に追加されると考えて間違いないと思います。 ただ、ここで言う「コード」というのは「ソースコード」ではなく「実行コード」の意味と考えてください。 > つまり、Fooクラスの宣言位置でFooのデストラクタをinline化した場合はstImplが宣言だけの不完全体であるために問題がありますが、 > stImplの定義が終わった後に明示すればstImplの詳細が見えるので問題がなくなるとの事でしょうか。 そのとおりです。 ただ、このメカニズムのみを考えているとNo2の回答に至るわけです。 このとき私はpimplを知りませんでした。 pimplの設計思想を踏まえたら、No6の回答に至ったわけです。 でも、そもそも、なんで不完全体の構造体をdeleteできちゃうんでしょうねぇ。 コンパイルエラーが出てくれたほうが親切だと思うんですが。 > 一般的にコンストラクタはstImplの定義よりも後に書かれるはすなので そのとおりです。 ただ、Foo::stImplをnewするコードと、mImplにそのアドレスを設定するコードが異なるコンパイル単位である場合、安全ではなくなってしまいます。 ただ、そのような構造はpimplの設計思想と反しているので、考慮する必要はないのでしょうね。

kary
質問者

お礼

色々お教えいただきましてありがとうございました。boostで警告がでない理由と、自作のスマートポインタで警告を出さない方法を知ることができました。とくにboostで用いられています「動的削除子」は今まで知らなかった考え方でしたのでとても勉強になりました。

その他の回答 (6)

  • 1839cc
  • ベストアンサー率54% (12/22)
回答No.6

> ただひとつ、何でstImplのデストラクタの話をしているのにFooのデストラクタが重要になるのかとの素朴な疑問があります。 > たぶん非常に基本的な話なように思いますので今後の勉強課題にしたいと思います。 ・・・答えちゃっても良いのかな? それはメカニズムがいまいち納得できないのでしょうか? それとも、論理的に納得できないのでしょうか? とりあえず、論理的な説明を以下にご回答いたします。 pimplイディオムの目的は、 stImplの実装をFoo以外から隠蔽することだったはずですよね? 逆に言えば、stImplの実装を知っているのはFooだけということになりますから、 FooはstImplの取扱いに関して全責任を負うべきです。 要するに、FooはstImplのデストラクタコールを保障する義務も持っているわけですね。 これが、Fooの実装方法が重要となる理由です。 (具体的な方法は、「stImplのデストラクタコールを全て 『stImplの詳細が見える位置』に記述する」だけです) ということは・・・ そもそもstImplのデストラクタコールをSharedPtrに委譲してしまう時点で Fooの設計は間違っていると言えそうです。 SharedPtrはpimplイディオムのデストラクタコールを保障していないのですから。 stImplの削除ファンクタをSharedPtrに渡すことができれば安心なのですが、 SharedPtrはそのようなインターフェイスも持っていません。 もっとも、boost::shared_ptrのように、 pimplイディオムのデストラクタコールも保障できるSharedPtrを 使用するという方法は良いアイディアだとおもいますよ。 そのように、MySharedPtrを拡張してみてはいかがですか? NO4,5を参考にすれば実現できると思います。 一方、FooのデストラクタをFoo.cpp内に実装するという、 私たちが提案した解決方法なのですが、 委譲先であるMySharedPtrの実装に依存しているため、 正直あまり誉められたものとは思いません。 (勉強になりました!自信ありとしていたのがバカみたいです・・・笑) このような方法よりは、SharedPtrを使用しない方がまだマシですね。

kary
質問者

お礼

度々のご回答、ありがとうございます。私なりにいろいろ調べた結果、以下のような事なのかなぁと考えます。「Effective C++ 3rd」の30項によりますと、クラスのデストラクタの先頭にはデータメンバのデストラクタを呼び出すコードがコンパイラにより自動的に追加されるとあります(これは概念を表現しただけのものかもしれませんが、理解する上では問題ないと思います)。今回の例の場合は、明示であれ暗黙であれFooのデストラクタは、 Foo::~Foo() {  mImpl.stImpl::~stImpl(); } のようになるのだと思います。mImplのデストラクタではstImplをdeleteするために、それを静的に解決しようとするとFooのデストラクタの位置が重要になるのだと思います。つまり、Fooクラスの宣言位置でFooのデストラクタをinline化した場合はstImplが宣言だけの不完全体であるために問題がありますが、stImplの定義が終わった後に明示すればstImplの詳細が見えるので問題がなくなるとの事でしょうか。また、以上の問題はmImplのdeleteを静的に解説する場合の問題であり、動的に解決する場合にはどこにFooのデストラクタがあっても問題がなくなるとの事だと思いました(Fooのコンストラクタで動的にdeleterを作成すればよい。一般的にコンストラクタはstImplの定義よりも後に書かれるはすなので)。当初とは異なる部分まで質問が及んでしまいまいたが、ご丁寧に回答頂きありがとうございました。

  • 1839cc
  • ベストアンサー率54% (12/22)
回答No.5

何度もすみません。 コードに間違いがいくつか。 MyScopedPtr のデストラクタが ~MySharedPtr になってますが、間違いです。 MyScopedPtr::destructor の型は AbstractDestructor* 型の間違いです。 となると、Destructor クラスの設計も変更ですねぇ。 struct AbstractDestructor {   virtual void operator() (void*) = 0; }; template <typename T> struct Destructor : public AbstractDestructor {   virtual void operator() (void* p)     { delete static_cast<T*>(p); } }; ちなみに、実は参考URLもほとんど読まずに書いてますので、boostとはかなり異なるかもしれません。 ただ、目的のことは実現できるはずです。

kary
質問者

お礼

参考URL、ありがとうございます。静的にデストラクタを解決しようとすると、FooのデストラクタからstImplの定義が見える必要があり、ただ、暗黙のデストラクタはinlineなので明示しないとバグになる。一方、deleteを動的に解決すればFooのデストラクタから定義が見えなくても問題ない(deleterを生成する箇所(コンストラクタ)から定義が見えればよい)とのことなのですね。非常に参考になりました。ただひとつ、何でstImplのデストラクタの話をしているのにFooのデストラクタが重要になるのかとの素朴な疑問があります。たぶん非常に基本的な話なように思いますので今後の勉強課題にしたいと思います。ありがとうございました。

  • 1839cc
  • ベストアンサー率54% (12/22)
回答No.4

> おそらく、Foo のコンストラクタの定義を main.cpp に移せば表示されなくなると思いますよ。 これはウソでした! ウソって言うより、そもそもコンパイルが通るわけがないです。 stImplの確保自にサイズが分からないわけですからね。 当然コンパイルエラーです。 > ただの偶然です。 ごめんなさ~い! コレもウソでした!! boostはちゃんと意識して設計されているようですね。 やはり、コンストラクタ時に、仮想関数を利用してデストラクタを決定しているようですね。 大体、以下のような機構ではないでしょうか。 詳細は参考URLをご覧ください。 ////////////////////// MyScopedPtr.h ///////////////////////////// class AbstractDestructor { protected:   virtual void Destruct(void*) = 0; }; template <typename T> class Destructor : public AbstractDestructor { public:   void operator(T* p) { Destruct(p); }    protected:   virtual void Destruct(void* p) { delete p; } }; tempalte <typename T> class MyScopedPtr {   T*        p;   Destructor<T>  *destructor;    public:   MyScopedPtr(T* p_) : p(p_), destructor(new Destructor<T>()) {}   ~MySharedPtr() { (*destructor)(p); }   void reset(T* p_)   {     (*destructor)(p);     delete destructor;     p = p_;     destructor = new Destructor<T>();   } }; ///////////////////////// main.cpp /////////////////////////////// int main() {   MyScopedPtr<Hoge> ptr(new Hoge);     // Destructor::Destruct は、MyScopedPtr::MyScopedPtrもしくはMyScopedPtr::resetで特殊化される。     // 二つの関数は普通 new と同時に呼ばれるため、Hogeの構造を知っているコンパイル単位に記述される。     // したがって、Destructor::Destruct は、~Hoge を知っていることが期待できる。   return 0; }

参考URL:
http://d.hatena.ne.jp/Cryolite/20060108
  • 1839cc
  • ベストアンサー率54% (12/22)
回答No.3

ちなみに、なぜ Boost では正しく呼ばれるのか、ですが、 あくまで予想ですが、おそらくデストラクタが呼ばれるまでの過程に、virtual関数コールが含まれているのでしょう。 virtual関数は、コンストラクタによって初期化されます。 質問者さんのコードでは、Foo のコンストラクタは Foo.cpp 側に書かれていました。 つまり、~stImpl() が見える位置で mImpl の virtual 関数が初期化されているのでしょう。 おそらく、Foo のコンストラクタの定義を main.cpp に移せば表示されなくなると思いますよ。 NO1さんの解説を見るとコンパイルが通らなくなるのかも・・・

kary
質問者

お礼

今回の例につきましてclとgccでいろいろ試してみたのですが、boostはいずれでも問題なく動きました(デストラクタを明示する・しないにかかわらず)。boostで問題がない理由は良くわからないというのが正直な所でした。ご回答ありがとうございました。

  • 1839cc
  • ベストアンサー率54% (12/22)
回答No.2

> 何でboostはうまくいくのか不思議に思い、質問させていただきました。 ただの偶然です。 バグはまったく別の場所にあります。 まず、そのバグの説明からしていきましょう。 Foo.hがインクルードされた際、二つのソースコードは以下のようになっています(大体のイメージです)。 ちなみに以下のコード中には、「暗黙のメンバ関数」とよばれる、コンパイラによって自動的に生成される関数を、いくつか追加してあります。 //////////////// Foo.cpp //////////////////// #include "MySharedPtr.h" class Foo {   struct stImpl;   MySharedPtr<stImpl> mImpl; public:   Foo();   ~Foo() {} }; struct Foo::stImpl {   stImpl() {}   ~stImpl() { cout << "xxx" << endl; }  // (1) }; Foo::Foo() : mImpl(new stImpl) {} //////////////// main.cpp //////////////////// #include "MySharedPtr.h" class Foo {   struct stImpl{     stImpl() {}     ~stImpl() {}  // (2)   };   MySharedPtr<stImpl> mImpl; public:   Foo();   ~Foo() {} }; int main() {   Foo foo; } お気づきになられましたでしょうか。 なんと ~stImpl() が二種類作られてしまっているのです。 なぜ、このようなことになるのでしょう。 質問者さんのコードでは、struct stImpl の定義が Foo.cpp に書かれています。 しかし、main.cpp は、Foo.h をインクルードしているだけですので、Foo.cpp にある定義を知ることは出来ません。 つまり、main.cpp からみると、定義のない構造体なのです。 この場合、stImpl にはデストラクタがありませんから、main.cpp のコンパイル時に、デストラクタなどのメンバ関数が暗黙的に生成されます。 それが、私の示したコードになります(実際は4種類の暗黙関数が生成されています)。 main関数から直接見えるデストラクタを辿ってみてください。 (2)のデストラクタにたどり着くはずです。 おそらく、Foo のデストラクタの定義を Foo.cpp に記述すれば、(1) 側のデストラクタが呼ばれるようになると思います(inline 関数として、Foo.h に定義すれば、(2)が呼ばれるのでは?)。 ただし、暗黙の関数がinline展開されるのは、コンパイラ固有の動作だと思われますので、すべてのコンパイラが同じ動作をするわけではありません。

kary
質問者

お礼

ご回答ありがとうございます。デストラクタを明示したら解決いたしました。また、ご指摘頂きましたようにデストラクタをinlineにした場合にはstImplのデストラクタが呼ばれませんでした。

  • rabbit_cat
  • ベストアンサー率40% (829/2062)
回答No.1

これは、Pimplの有名な問題でして、 本来の、正しい解決法は、 Foo.hに、Fooクラスのデストラクタの宣言 ~Foo(); を明示的に書いて、 Foo.cppの一番最後(Foo:stImplの定義が終わったあとに) Foo:~Foo() {} とFooの(空の)デストラクタを定義することです。 boost::scoped_ptr や boost::shared_ptrでは、 boost::checked_deleteてやつが使われていて、不完全なクラスのdeleteが起きないようにチェックしてくれます。 ------ boost::shared_ptrでうまくいってしまう理由はよくわかりません。 Pimpleのポインタを持つだけなら、boost::scoped_ptrで十分でして、boost::shared_ptrはオーバースペックなわけですが、 Fooの明示的なデストラクタ宣言がない状態で、boost::shared_ptrを使うと、checked_delete でちゃんとコンパイルエラーが起きます。 おそらくshared_ptrは、参照カウンタを作るために、コンパイラがFooクラスの実体が作られるのが遅くなって、コンパイラがFooクラスの実体を作るときにFoo::stImplクラスを知っている状態になっているからでしょう。 ただし、shared_ptrでうまくいっているのは、あくまで、たまたまで、コンパイラを変えたりすればうまく行くとは限らないはずです。 正しい解決法は、上に書いたように、Foo に(空の)デストラクタを定義することです。

kary
質問者

お礼

ご回答頂きましたようにデストラクタを明示したら解決しました。ありがとうございました。また、もしこの件について触れられている書籍をご存じでしたらご紹介頂けないでしょうか。「プログラミングc++」「Effective c++」「Effective STL」「Efficient c++」「Efficient c++ style」「Modern c++」当たりを呼んで勉強していますが、もしかしたら見落としがあったかもしれませんし、別の書籍でしたら購入を検討してみたいと思っています。よろしくお願いします。

関連するQ&A