pimpl と abstract class とわたし

http://togetter.com/li/42968

全体的に違和感があったので、少し書きます。

yreeen: ファクトリなら機能追加を見越してバージョン番号を渡すように引き数作っとけばいいし。pimplの方が優れている部分が正直思い浮かばないんだけど何かあるんですか教えてえりゅい人!

pimpl でも抽象クラスとファクトリを利用することはできます。

// header /////////////////////
class c {
public:
  c(params);
  void f();
private:
  class c_impl;
  shared_ptr<c_impl> pimpl_;
};
// source /////////////////////
class c::c_impl {
public:
  ~c_impl(){}
  virtual void f_impl() = 0;

  static shared_ptr<c_impl> factory(params);
  class a;
  class b;
};

class c::c_impl::a { ... };
class c::c_impl::b { ... };

shared_ptr<c::c_impl> c::c_impl::factory(params){ ... }

c::c(params) : pimpl_(c_impl::factory(params)) {}
void c::f(){
  pimpl_->f_impl();
}

pimpl はただインターフェースと実装を分けるだけのテクニックですから、具体的な実装は何でもありです。template やファンクタを利用することもできます。

SubaruG: 最初は効率を無視して作り、後からあらゆる手段を使って効率を高める、ということが行いやすい。 そう、 pimpl ならね。

この辺の話ですね。まあ動的な多態の実行時コストが無視できなくなって、静的な多態に移行するような状況なら pimpl もやめたいかもしれません…

抽象クラスに対して pimpl に弱いところがあるとすると、基底クラスを公開しないために、利用者が派生クラスを実装できない点です。
が、クローズであることは同時に強みでもあります。
あとは関数呼び出し一回分遅いですが、仮想関数でない通常の関数であることを考えると、これくらいは安いので無視していいでしょう。

SubaruG: pimpl の強みは int と同じように振舞うクラスを書ける点であって、継承を使うなら pimpl に意味はない。

int として振る舞うクラス、が int のような組み込みの値型と同等のインターフェースを持つことを意味するのなら、使い物にはなりませんが抽象クラスのインターフェースをそう定義することもできます。
恐らく完全型なのでスタックにのせられるということが言いたかったのだと思われますが、スタックにのせる意味なんていうのは、文法変わる、アロケーションコスト下がる、スタックに所有権を委譲させられる、の三つぐらいしかありません。
文法はゴルフ的には意味がありますが、その程度です*1アロケーションコストは、そもそも pimpl はアロケーション回数とほんの少しのメモリを犠牲にした、実行時とコンパイル時のトレードオフだということを思い出すべきですね。所有権はスマートポインタでヒープ上のオブジェクトでも同等のセマンティクスを与えられるのでちょっと弱い。

継承を使う意味ですが、pimpl とは「インターフェース定義クラス」と「実装クラス」を分けるテクニックです。*2多態性は通常実装を差し替えるために存在するので、インターフェース定義クラスである公開されるクラスに仮想関数を定義する意味は SubaruG さんの言うとおり、あまりありません。というか基本的にしてはいけないでしょう。
ですが、上で見たように実装クラスに継承を用いることはできますし、意味もあります。

SubaruG: 使う側の都合としては、ファイルフォーマットによって内部表現が変わる、なんてのは全く関係ないことで、とにかく使えればいいのです。

抽象クラスでも、ファイルフォーマットによって内部表現が変わることを隠蔽できていると思うのですが…
まあそれはよくって、外部に公開する物を pimpl にすると、詳細がクローズなために拡張できなくて泣くこともあります。
例えば様々なファイルフォーマットに対応するために利用したライブラリが、対応していないフォーマットのファイルを扱う必要性が発生した時。抽象クラスとファクトリ関数による実装なら、クラスの追加と新しいファクトリ関数の追加で済みますが、pimpl で実装(のインターフェース)が完全に隠蔽されていると、ちょっと困ったことになります。
ライブラリなんかを実装する際には、オープンにするかどうか、ちゃんと考えて選択する必要がありますね。

まとめ

pimpl は単に実装の詳細を隠蔽し、コンパイル時の依存を低減するテクニックで、それ以上でもそれ以下でもありません。他の全ては後からついてくるおまけです。

一方抽象クラスは、実装の詳細の隠蔽はおまけとしてついてくるもので、本来の目的は極論すると vtbl です。実装のインターフェースが公開されるため、高い拡張性を持ちます。

どちらも同様に依存関係を断ち切ることはできますが、本来の目的や、メリット、デメリットは全く異なります。

ということがちゃんと認識されていって欲しいな、と思いました。


おまけ

SubaruG: スマートポインタの知識を要求するライブラリはダメライブラリです。生ポインタを返すライブラリは滅びるべき。

一般的にセマンティクスの浸透しているスマートポインタを返すのと、セマンティクスの不明な値型を返すのと、どちらかがいいかは場合によると思います。スマポのセマンティクスはダメだ、っていう時にはそうするしかないと思いますが…

でも生ポはあかんよ。

*1:値型のセマンティクスは、場合によっては非常に重要なのでこれは極論です

*2:実装クラスにもインターフェースはあるので、インターフェース定義クラスのインターフェースと実装クラスのインターフェース、なんて言っていると、段々意味が分からなくなってくるのじゃ