redboltzの日記 このページをアンテナに追加 RSSフィード

2008-10-03

[]pimplイディオムをscoped_ptrを使って実現するときの注意

C++ではインターフェースと実装の分離を実現するために、pimplイディオムというアプローチを採ることがある。

どんなアプローチかというと

  • 対象のクラスを公開用クラスと実装用クラスの2つに分離する。
  • 公開クラスのヘッダファイルで行うこと
    • 公開用メンバ関数の宣言を行う。
    • private領域に、実装用クラスのポインタのみを保持する。
    • そのため、実装用クラスを宣言(先行宣言)する。
  • 公開クラスのソースファイルで行うこと

実装クラスは普通に実装すればよい。

さて、見ていこう。

クライアントであるmain.cppでは、公開クラスのヘッダのみインクルードすればよい。

main.cpp


#include "MyClass.h"

int main()
{
	My m;
	m.some();
}

公開ヘッダのMyClass.hでは、実装用クラスMyImplを先行宣言すると共にポインタを保持している。

ポインタの解放を自動で行わせるため、boost::scoped_ptrを利用している。

デストラクタの宣言が、コメントアウトされているが、後で触れるので、今は無視して欲しい。

MyClass.h


#if !defined(MyClass_h)
#define MyClass_h

#include <boost/scoped_ptr.hpp>

class MyImpl;
class My {
public:
	My();
//	~My();
	void some() const;
private:
	boost::scoped_ptr<MyImpl> pi_;
};

#endif // MyClass_h

公開用クラスの実装ファイルMyClass.cppでは、pi_の初期化を行うと共に、some()をpi_->some()にdelegateしている。

デストラクタの宣言が、コメントアウトされているが、後で触れるので、今は無視して欲しい。

MyClass.cpp

#include "MyClassImpl.h"

My::My():pi_(new MyImpl) {}

// My::~My() {}

// delegate member function

void My::some() const
{
	pi_->some();
}

実装用クラスのヘッダファイルMyClassImpl.hでは、実装用クラスの定義を行う。

ここでは、気兼ねなくクライアントに公開したくないメンバ変数などを使ってかまわない。

MyClassImpl.h

#if !defined(MyClassImpl_h)
#define MyClassImpl_h

#include "MyClass.h"

class MyImpl {
public:
	MyImpl();
	void some() const;
private:
	int data_;
};

#endif // MyClassImpl_h

そして、実装用クラスのソースファイルは、まあ、やりたいことを書けばよい。

MyClassImpl.cpp

#include "MyClassImpl.h"
#include <iostream>

MyImpl::MyImpl():data_(100)
{
}

void MyImpl::some() const
{
	std::cout << data_ << std::endl;
}

さて、これをコンパイルすると、

g++ -o main.o -c -Wall -ansi -I/usr/include/boost-1_33_1  -O2  main.cpp
/usr/include/boost-1_33_1/boost/checked_delete.hpp: In function `void boost::checked_delete(T*) [with T = MyImpl]':
/usr/include/boost-1_33_1/boost/scoped_ptr.hpp:77:   instantiated from `boost::scoped_ptr<T>::~scoped_ptr() [with T = MyImpl]'
main.cpp:5:   instantiated from here
/usr/include/boost-1_33_1/boost/checked_delete.hpp:32: error: invalid application of `sizeof' to incomplete type `MyImpl'
/usr/include/boost-1_33_1/boost/checked_delete.hpp:32: error: creating array with size zero (`-0x000000001')
/usr/include/boost-1_33_1/boost/checked_delete.hpp:33: error: invalid application of `sizeof' to incomplete type `MyImpl'
/usr/include/boost-1_33_1/boost/checked_delete.hpp:33: error: creating array with size zero (`-0x000000001')
/usr/include/boost-1_33_1/boost/checked_delete.hpp:34: warning: possible problem detected in invocation of delete operator:
/usr/include/boost-1_33_1/boost/checked_delete.hpp:30: warning: `x' has incomplete type
MyClass.h:6: warning: forward declaration of `struct MyImpl'
/usr/include/boost-1_33_1/boost/checked_delete.hpp:34: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined.

こんな風にmain.cppでコンパイルエラーが出る。(boostが古いのはご愛敬。最新でも変わらないはず。)

何が起こっているかというと、main.cppからインクルードしているMyClass.hで、boost::scoped_ptr<MyImpl>を実体化(instantiate)しようとしたが、MyImplの完全な定義が無い(先行宣言のみである)ため、これに失敗したというのだ。

MyImplの完全な定義を要求しているのは、MyImplを破棄するboost::checked_deleteという仕組みにおいてである。

内部では最終的にdelete pi_ を行うのだが、pi_のデストラクタ、すなわち、MyImplのデストラクタを呼び出すためには、クラスの完全な定義が必要というわけだ。

さて、ここで、MyClass.hにおける、MyImplの先行宣言をやめ、完全な定義を行うために、#include "MyClassImpl.h"などとやってはけない。

そもそも、pimplイディオムは、実装を公開したくないという動機で導入しているはずだ。

よって、インクルードファイルが別れたとしても、これ(MyClassImpl.h)をインクルードしては意味がない。

ここで、行うべき対応は、MyClass.hとMyClass.cppで、コメントアウトしていたデストラクタを宣言、定義することである。

これで、main.cppでエラーは出なくなり、期待通りの動作をするはずだ。

さて、これはなぜだろうか?

実は、デストラクタの定義を行わない場合、コンパイラによって、非仮想でインラインで、何も処理しないデストラクタが自動的に挿入される(のと同じ動作になる)のだ。

つまり、Myの定義は、こんな感じになる

class My {
public:
	My();
	~My() {}
	void some() const;
private:
	boost::scoped_ptr<MyImpl> pi_;
};

scoped_ptrテンプレートの実体化は、それが呼び出されるところで行われる。デストラクタでは、先ほどのchecked_deleteが呼び出される。

そして、このクラス定義は、main.cppから呼び出され、インラインのデストラクタが存在し、MyImplは先行宣言しかない。

よって、先ほどのエラーが出る状況になるわけだ。

で、デストラクタを、明示的にMyClass.cppで定義することで、デストラクタがインラインとなることが抑制され、scoped_ptrテンプレートも、MyClass.cppにてのみ実体化するようになる。

当然、MyClass.cpp では、MyImplの完全な定義をincludeしている(newも行うし、delegateメンバ関数も呼び出すので)。

これで一件落着である。

デストラクタを明示的に定義することで、scoped_ptrテンプレートの実体化位置を制御しよう。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/redboltz/20081003/1222991824