ご無沙汰しております。
久々に、DirectX9のライブラリを作りなおしてみようと考え、プログラミングを始めました。
今回は、Effective C++ 第3版に載っている、実装とインターフェイスの分離をするための、pimplイディオムについてお聞きしたいと思います。
私は、ある基底クラスで「実装の隠蔽」+「ファイル間の依存関係によるコンパイル時間増大の防止」を行うために、pimplイディオムを使用しました。
その基底クラスから継承した時、基底クラスにある実装クラスも継承しなければならないことがわかりました。
しかし、基底クラスを継承したものを別ファイルに置きたいと思ったのですが、基底クラスの実装クラスは隠蔽されているため、もちろん基底クラスの実装クラスを継承する事は出来ませんでした。
別のファイルに継承クラスを作ることにすると実装クラス用のヘッダーファイルを作ることになりますが、これは実装の隠蔽に反してしまいます。
私が考えたのは、pimplイディオムは、
・継承に使われるクラスには使用しない。
・全てのクラスに使うのではなく、それらのクラスをまとめたパッケージとしてのクラス(インターフェイスクラス?)に使用する。
であるべきだと思いました。
皆さんはどのようにお考えですか?
今回勉強ついでに、初めてpimplイディオムを使用したのですが、よくわかっていないことがまだ多いため、ここで質問させていただきました。
よろしくお願いします。
pimplイディオムの使用場所
Re:pimplイディオムの使用場所
> 別のファイルに継承クラスを作ることにすると実装クラス用のヘッダーファイルを作ることになりますが、これは実装の隠蔽に反してしまいます。
公開用のヘッダと実装内部でしか使わないヘッダにわければよいのでは?
公開用のヘッダと実装内部でしか使わないヘッダにわければよいのでは?
Re:pimplイディオムの使用場所
>基底クラスから継承した時、基底クラスにある実装クラスも継承しなければならないことがわかりました。
実装クラスに依存しないように、設計を見直すべきです。
>別のファイルに継承クラスを作ることにすると実装クラス用のヘッダーファイルを作ることになりますが、
>これは実装の隠蔽に反してしまいます。
誰に対して隠蔽しようとしていますか?
隠蔽すべき相手はパッケージ利用者であって、
パッケージの提供者には隠蔽する必要ないですよね?
実装クラスに依存しないように、設計を見直すべきです。
>別のファイルに継承クラスを作ることにすると実装クラス用のヘッダーファイルを作ることになりますが、
>これは実装の隠蔽に反してしまいます。
誰に対して隠蔽しようとしていますか?
隠蔽すべき相手はパッケージ利用者であって、
パッケージの提供者には隠蔽する必要ないですよね?
Re:pimplイディオムの使用場所
> Bridgeパターンを適用することで、問題は解決するように見えますが。
Pimplイディオムでは、クラスの定義では
class foo
{
public:
...
private:
struct impl;
imple* pimpl;
}
のように、実装クラス(上ではimpl)を不完全型として宣言することで、仮想関数さえも隠蔽します。
ここでBridgeパターンを使ってしまうと、うまみが半減してしまいます。
Pimplイディオムでは、クラスの定義では
class foo
{
public:
...
private:
struct impl;
imple* pimpl;
}
のように、実装クラス(上ではimpl)を不完全型として宣言することで、仮想関数さえも隠蔽します。
ここでBridgeパターンを使ってしまうと、うまみが半減してしまいます。
Re:pimplイディオムの使用場所
> Pimplイディオムでは、クラスの定義では
>
> class foo
> {
> public:
> ...
> private:
> struct impl;
> imple* pimpl;
> }
>
> のように、実装クラス(上ではimpl)を不完全型として宣言することで、仮想関数さえも隠蔽します。
> ここでBridgeパターンを使ってしまうと、うまみが半減してしまいます。
クラス定義内での隠蔽という点に関して、同様のことがBridgeパターンでも
出来る気がするのですが。
class imple;
class foo
{
public:
virtual void setImpl(imple * pimpl);
...
private:
imple* pimpl;
}

>
> class foo
> {
> public:
> ...
> private:
> struct impl;
> imple* pimpl;
> }
>
> のように、実装クラス(上ではimpl)を不完全型として宣言することで、仮想関数さえも隠蔽します。
> ここでBridgeパターンを使ってしまうと、うまみが半減してしまいます。
クラス定義内での隠蔽という点に関して、同様のことがBridgeパターンでも
出来る気がするのですが。
class imple;
class foo
{
public:
virtual void setImpl(imple * pimpl);
...
private:
imple* pimpl;
}

Re:pimplイディオムの使用場所
> クラス定義内での隠蔽という点に関して、同様のことがBridgeパターンでも
> 出来る気がするのですが。
>
> class imple;
>
> class foo
> {
> public:
> virtual void setImpl(imple * pimpl);
> ...
> private:
> imple* pimpl;
> }
>
これだと、外部からpimplを破壊できるので、カプセル化が破綻しませんか?
あとから実装をすり替えられるのは、Pimplイディオムとはちょっと違うと思います。
> 出来る気がするのですが。
>
> class imple;
>
> class foo
> {
> public:
> virtual void setImpl(imple * pimpl);
> ...
> private:
> imple* pimpl;
> }
>
これだと、外部からpimplを破壊できるので、カプセル化が破綻しませんか?
あとから実装をすり替えられるのは、Pimplイディオムとはちょっと違うと思います。
Re:pimplイディオムの使用場所
自分は pimpl は基本的に使わないのですが、もし実装を隠蔽した状態でポリモフィックな処理を行いたいということになれば、template や Type Erasure を使うでしょうね。
あと、少し前に半分遊びで以下のようなコードを書きましたが、さすがに書くのがめんどすぎるのでやめました。参考までに。
http://ideone.com/FaSbJ
あと、少し前に半分遊びで以下のようなコードを書きましたが、さすがに書くのがめんどすぎるのでやめました。参考までに。
http://ideone.com/FaSbJ
Re:pimplイディオムの使用場所
> これだと、外部からpimplを破壊できるので、カプセル化が破綻しませんか?
意図的な破壊(正式な手続きを踏んだ破壊)になるんで、破綻に当てはまらないと思います。
> あとから実装をすり替えられるのは、Pimplイディオムとはちょっと違うと思います。
仰る通りです。
しかし、ぬっちさんの質問は「Pimplイディオムの使い方」ですが、
質問のポイントは「実装の隠蔽とインタフェースの拡張性の両立するには?」に
あると思います。
そうすると、自作ライブラリ/パッケージの利用者に対して隠蔽し、かつ拡張を認める場合は
Bridgeパターンが適切だと思います。
#ちなみに、自作ライブラリ/パッケージの利用者に対して隠蔽し、かつ利用者/提供者に拡張を認めない場合は
#Pimplイディオムが適切だと思います。
#また、自作ライブラリ/パッケージの利用者に対して隠蔽し、かつ提供者に拡張を認める場合は
#たかぎさんの仰るように、ヘッダを公開用と実装用で分けるのが適切だと思います。
意図的な破壊(正式な手続きを踏んだ破壊)になるんで、破綻に当てはまらないと思います。
> あとから実装をすり替えられるのは、Pimplイディオムとはちょっと違うと思います。
仰る通りです。
しかし、ぬっちさんの質問は「Pimplイディオムの使い方」ですが、
質問のポイントは「実装の隠蔽とインタフェースの拡張性の両立するには?」に
あると思います。
そうすると、自作ライブラリ/パッケージの利用者に対して隠蔽し、かつ拡張を認める場合は
Bridgeパターンが適切だと思います。
#ちなみに、自作ライブラリ/パッケージの利用者に対して隠蔽し、かつ利用者/提供者に拡張を認めない場合は
#Pimplイディオムが適切だと思います。
#また、自作ライブラリ/パッケージの利用者に対して隠蔽し、かつ提供者に拡張を認める場合は
#たかぎさんの仰るように、ヘッダを公開用と実装用で分けるのが適切だと思います。
Re:pimplイディオムの使用場所
Bridge パターンにするということであれば、pimpl イディオムも必要なく、インターフェースを公開するだけでいいですね。
// ここら辺は公開する // ユーザはこの interface の実装と hoge の機能を適当に拡張することができる struct interface { virtual ~interface() { } virtual void foo() = 0; }; struct hoge { interface* p_; hoge(interface* p) : p_(p) { } interface* operator->() const { return p_; } void bar() { /* 何か便利な実装 */ } }; // ここら辺は公開しない(でいいのかな?そうなるとこのクラスを生成するためのファクトリが必要になるけど) struct interface_default : interface { virtual void foo() { /* 何かの実装 */ } int a; // 何かのデータ };
Re:pimplイディオムの使用場所
たかぎさん、ぽこさん、めるぽんさん 返信ありがとうございます。
私の質問のポイントは、ぽこさんの仰るとおり「実装の隠蔽とインタフェースの拡張性の両立するには?」ということです。
このポイントに対しては、Bridgeパターンを多少勉強してみましたが、これが一番望む結果を生むと思われます。
皆様の親切な回答のため、この問題を解決できそうです。
Bridgeパターンを知ったところで疑問が生じてきました。
・Bridgeパターンはpimplイディオムと同様に、コンパイル時のファイル依存性の防止は同じように実現できるのか?
・Bridgeパターンとpimplイディオムの主な違いは?(具体的には、どのような場所でそれぞれ使い分けるのか?)
この2つです。
ここで質問させていただいたことでデザインパターンの重要性に改めて気づかされました。
どうもありがとうございました。
私の質問のポイントは、ぽこさんの仰るとおり「実装の隠蔽とインタフェースの拡張性の両立するには?」ということです。
このポイントに対しては、Bridgeパターンを多少勉強してみましたが、これが一番望む結果を生むと思われます。
皆様の親切な回答のため、この問題を解決できそうです。
Bridgeパターンを知ったところで疑問が生じてきました。
・Bridgeパターンはpimplイディオムと同様に、コンパイル時のファイル依存性の防止は同じように実現できるのか?
・Bridgeパターンとpimplイディオムの主な違いは?(具体的には、どのような場所でそれぞれ使い分けるのか?)
この2つです。
ここで質問させていただいたことでデザインパターンの重要性に改めて気づかされました。
どうもありがとうございました。