スマートポインタ考察 I
いや、Iと付いてるからって、別にIIを予定している訳でもなんでもない。
私が初めてスマートポインタに出会ったのは何年前だっただろうか。
その頃から私はスマートポインタが好きである。そんな感情的なことはさておくとしても、
テンプレートが横行する最近のC++事情において、何よりもまずこれを
語らずにはいられまい。そこらのライブラリに入ってるクラスでも
スマートポインタが一番使いやすい(自分のコードに組み合わせやすい)
し、実際に使っている、というのは皆さん経験があるのではと思う。
「それならコンテナから行けよ」って言いたい人、我慢してくれたまえ。
まあとにかく、スマートポインタについて少々書いてみよう。
断っておくが、この文章は私の個人的経験と考察にのみ基づくもので、
ちゃんとした文献やC++の仕様に根拠を置いていることは稀である。
ご注意いただきたい。
おそらくスマートポインタの定義とは「自動的にポイント先のメモリを解放して
くれるポインタ」とすることができ、このあたりが一番広い定義ではないだろうか。
「自動的に」とは、プログラマがメモリ解放コード(deleteやfree()など)を
書かなくてもよい(あるいは書けなくてもよい)ということであるが、
これを実現するために実際にはデストラクタが利用される。そして、
ある程度通常のポインタと同様に扱えるように、operator->やoperator*などが
オーバーロードされる。よって実際的には「->や*が使えてデストラクタで
メモリ解放してくれるポインタラッパークラス」ということになる。
C++標準ライブラリのauto_ptrはこれの代表的なものである。
しかし、このauto_ptrがSTLのコンテナたちと相性が悪い。これはよく言われて
いるのでwebで他の情報を探してもらえばよいと思う。
そこでコンテナに入れられるようにと他の方法でownership管理をするスマート
ポインタが考えられて来た。Boostのshared_ptrなどもその一つである。
このスマートポインタに代表されるように、参照カウントを用いてオブジェクト
の寿命を管理する、という方法が解決策として一般的である。拙作スマート
ポインタもこの方法を用いている。他にもリンクリストを用いる方法などが
あるし、カウントを使うにしても専用のメモリプールを用いたり、
ガービジコレクション的なものを実装するなど、いろいろある。
さてここからは、スマートポインタはどうあるべきか、という話をしたい。
->や*のオーバーロードをしていないスマートポインタクラスはないだろう。
(というか、そう呼ばないだけか。)
すなわち、通常のポインタと同じように使えることを目指していることは
疑い得ない。ところが、世界にはスマートポインタの実装がたくさんあるのだが、
キャストができるスマートポインタはそう多くない。完全なキャストとは
言わないまでも、派生クラスへのスマートポインタから基底クラスへの
スマートポインタへの代入くらいはできてよさそうなものである。Boostの
shared_ptrはこれができるスマートポインタの一つである。
逆に言うと、ちまたに多くある(大部分は個人実装の)スマートポインタは、
通常のポインタならば当たり前の代入すらコンパイルエラーではじかれる
のである。これではとても「ポインタ」とは呼べない。
C⇒C++でポインタ(または参照)の重要性は多態まわりでさらに
増しているのに、こんなことでは困るのである。
私が考える最低限の条件は
- operator->が使える
- operator*が使える
- null pointerがあり得る
- operator=(コピー可能)
などの基本的なものに加え、
- 派生クラスへのポインタから基底クラスへのポインタへ代入できる
- ポインタの異同判断(例えばoperator==)が可能
である、これを満たしてこそ「スマートポインタ」と呼べるし、
狭義のスマートポインタの定義はこれである。
さらにできれば満たすべき条件は
- ダイナミックキャストができる
- if(p)みたいなことができる
- 配列に対して[]が使える
である。
これらの機能を全部満たした参照カウント型スマートポインタは
非常に少ない。それどころかまったく「ポインタ」でないのにスマートポインタ
と名乗っていることがある。注意されたい。
ところが、これらを全部満たしていれば
有無を言わず合格点をあげられるかというと、そうでもないのである。
余計な機能をくっつけていると、マイナス点が付く。
ここで「何が必要で何が余計か」の判断が重要になってくるのだが、
それにまつわる話を以下に書くとしよう。
私がスマートポインタの設計において重要だと考えるのは、
ポインタを完全にラップすることである。
スマートポインタはdeleteをプログラマがしなくてよいようにし、
逆にさせないようにすることでバグを減らす。
「deleteし忘れ」から逃れる一方で「二重delete」からも逃れられる
ものでなければならない。deleteし忘れは伝統的なauto_ptrが
そうあるようにデストラクタにdeleteを埋め込むことで解決してきた。
そして今度はそれによって浮かび上がった二重delete問題をどのように
解決するかでスマートポインタ設計者は頭を悩ませて来たのである。
そこでauto_ptrは「"ポインタ所有権"があるスマートポインタ
オブジェクトしかdeleteしない」&「所有権をコピーの際に移す」
をひとつの解決策としたし、参照カウント型は参照カウントによって
それを解決したのである。
このようにしてがんばって(カウントまで埋め込んで)二重deleteを
回避しようとしているのに、プログラマに勝手にdeleteされては
たまったもんではない。
よって、オブジェクト指向なんて持ち出すまでもなく、ポインタラッパー
クラスは完全に外からの生ポインタへのアクセスを排除すべきなのである。
もちろんこれは理想であるが、現実にしても理想に近いほどよいのは
言うまでもない。
したがって、どこまで生ポインタを隠蔽しているかが、プログラマの
誤ったdelete(直接に書いていなくても内部でdeleteされ得る関数や
クラスに入れてしまうなど)を回避できる可能性を示している。
いわば「newと同時にスマートポインタに入れられ、二度と外には出ない」
が究極のポリシーである。
このように考えると、生ポインタへの暗黙の変換演算子(operatorT*)
なんて付けてるやつは論外である。これを付けた本人は「これがないと
これまでのポインタを引数にとるライブラリなんかと一緒に使うときに
不便だから」などと言うのであろう。そのような(参照を使っていない)
旧式のライブラリ(もしくはAPI)を使わざるを得ないのはかわいそうだが、
だからといってスマートポインタを暗黙に生ポインタに変換してよい
理由にはならない。その暗黙の変換機能のせいで、生ポインタ型に
うっかり代入していてもコンパイルエラーも出ず、その生ポインタを
別のところでdeleteした日には二重deleteの憂き目に会う。
T* get_ptr()みたいなメソッドを用意して明示化している(かつ暗黙の
変換はない)のはそれに比べればまだマシであるが、そんなものは
ハナから無いほうがよい。時にはこういうメソッドのコメントに「これで
取得したポインタをdeleteしたりスマートポインタに入れたり
しないでください」などとご丁寧に書いてある。じゃあそんなメソッド作るな、
と言いたくなる。わざわざスマートポインタから生ポインタを取り出さ
なきゃならないような設計なら最初から入れるな、である。reset()
みたいなメソッドが付いてて、「これでスマートポインタの中をnull
(or別のポインタ)にすれば二重deleteにはならずに外でdeleteできて・・・」。
あー無意味。このような操作は、結局、スマートポインタが保持している生ポインタの
ライフタイムをプログラマが逐一管理しなければならないということであり、
スマートポインタのそもそもの恩恵にまったくあやかれない。
これでは何のためにスマートポインタに入れたのかさっぱりである。
よって、スマートポインタを用いる場面も重要であるが、
使うなら全部スマートポインタに任せなさい、ということである。
それには全部任せられるような機能が付いていなければならないし、
スマートポインタのままあっちやこっちやできる設計になっていなければ
ならないのだが、そんなのはたいして難しいことではない。その辺の
exampleは別の機会に書くとして、今回はここで筆を置こう。
- originally written at 2003.Oct.18
- subtly modified at 2004.Feb.28