2015-10-25-01-STL書籍の掲載コードをCL-STLで書いてみよう-13
>> Site top >> weblog >> 月別アーカイブ >> 2015年10月のlog >> 2015-10-25-01-STL書籍の掲載コードをCL-STLで書いてみよう-13
最終更新日付:2015/10/25 01:00:00
STL書籍の掲載コードをCL-STLで書いてみよう-13
2015 年 10 月 25 日
今回は Effective STL の第33項、「ポインタのコンテナには注意してremove風アルゴリズムを使おう」をやってみよう。
今回の主なトピックは、「ポインタのコンテナで迂闊に remove を使うと、リソースリークするよ」というものだ。これが CL-STL にどう関係するのか。ガベージコレクタが備わっている Common Lisp で、リソースリークの心配は必要だろうか。結論から書いてしまえば、リークの心配はないが後始末の心配はあるよね、という話。そして、単純に同じコードを STL で書けないような状況についても少し。
まずは書籍を追いかけていこう。Widget クラスに isCertified メンバ関数があるものとして、それが偽を返すようなオブジェクトを vector シーケンスから削除するために remove-if と erase の組み合わせを使用している。
class Widget { public: //... bool isCertified() const; //... }; vector<Widget*> v; //... v.push_back( new Widget ); //... v.erase( std::remove_if( v.begin(), v.end(), std::not1( std::mem_fun( Widget::isCertified ) ) ), v.end() );
まぁ、なんていうか、これでリソースリークの一丁あがりだ。remove-if によって削除される要素は、それよりも後ろにいる削除されない要素によって上書きされる。『要素』と書いたのはここではポインタのことだ。だから削除された要素は delete するタイミングのないまま手の届かないところに行ってしまった。
これとまったく同じことを CL-STL でやると以下のようになる。クラス widget と、そのインスタンスをパラメータとして取る総称関数メソッド is-certified があるものとする。同じように remove-if して erase してみよう。
(defgeneric is-certified (obj)) (defmethod is-certified ((obj widget)) ;;... ) (let ((new stl:vector)) ;;... (stl:push-back v (new widget)) ;;... (stl:erase (stl:remove-if (stl:begin v) (stl:end v) (stl:not1 #'is-certified)) (stl:end v)))
で、これでどうなるだろう? 削除された widget オブジェクトはたしかに手の届かないところにいってしまった。それは同じだ。でもそこはガベージコレクタがちゃんと面倒を見てくれる。だから、CL-STL の世界ではこれは問題にはならない。そうだよね?
でも、状況によっては良くないんだ。それは、冒頭に書いた「リークの心配はないが後始末の心配」がある場合だ。仮に widget クラスがなんらかの後始末(ファイルハンドルをクローズするとか)を必要とするクラスだった場合、上記のコードはやっぱり問題があることになる。後始末をしないままオブジェクトが手の届かないところに行ってしまうからだ。そして CLOS のクラスにはデストラクタというものがないから、そこにも期待できない。あったとしても、ガベージコレクタがオブジェクトを始末するタイミングには何の保証もないはずだから、やっぱりダメだ。これをなんとかしたければ、remove-if のかわりに partition を使う方法とか、色々ある。このページの後半で扱っているのはそんな方法のひとつだ。
余談になるけれども、上記のコードが STL のコードと異なる点として、std::mem_fun が必要ないというのがある。これは、CL-STL では関数テンプレートとして実装されているアルゴリズムもコンテナなどのメソッドも、すべてが総称関数(あるいは普通の関数)になっているからだ。とはいえ、表記を揃えるだけのために stl:mem-fun なんてのも用意されてはいるのだが。
で、本題に戻ろう。ここからちょっと込み入った話になる。書籍では、delAndNullifyUncertified という関数を用意して、std::for_each でもってシーケンス内の Widget ポインタの一部を delete してさらに null ポインタに変更するというのをやっている。
void delAndNullifyUncertified( Widget*& pWidget ) { if( !pWidget->isCertified() ) { delete pWidget; pWidget = 0; } } std::for_each( v.begin(), v.end(), delAndNullifyUncertified ); v.erase( std::remove( v.begin(), v.end(), static_cast<Widget*>( 0 ) ), v.end() );
率直に言って、この delAndNullifyUncertified 関数というのは正直あまり良くないと思う。というのも、ポインタの参照をパラメータとして取ることによって、アルゴリズム std::for_each を使ってシーケンスの変更をしているからだ。C++ 標準では、for_each は「25.1, non-modifying sequence operations」というセクションにある。だから、これがシーケンスを変更するという目的で存在するものではないと思うのは個人的な感覚で片付けられることではないだろう。コードをぱっと見た時に、そこでシーケンスを変更していることがわかりにくいのではないか、ということだな。自分としては、こういう場合には transform を使用するべきだと思う。
気に入らないもうひとつの理由は、CL-STL でこれを再現できないからだ。テンプレートではなく CLOS と総称関数(およびマクロ)ですべてを実現しようとしている CL-STL では、当然ながら参照を渡すということができない。だから、delAndNullifyUncertified 関数と同じことをする関数を作ることはできない。まるで最初に書いた理由で後に書いたことを正当化しているみたいだけれど、そういう意図はない。仮に参照渡しを Common Lisp がサポートしていたとしても、delAndNullifyUncertified 関数のことはやっぱり気に入らないだろう
というわけで、transform でもって同じことを実現する CL-STL コードを書いて今回を終わりにしよう。関数 del-and-nullify-uncertified は、オブジェクトを受け取って certified かどうかをチェックし、そうなら単純にオブジェクトを返す。そうでなければ finalize-widget で後始末をして nil を返す。これを使って transform をかける。このアルゴリズムは通常、変換結果を別シーケンスに書き出すのに使用するが、シーケンス内でインプレースに変換することももちろんできる。
(defun del-and-nullify-uncertified (obj) (if (is-certified obj) obj (progn (finalize-widget obj) nil))) (stl:transform (stl:begin v) (stl:end v) (stl:begin v) #'del-and-nullify-uncertified) (stl:erase v (stl:remove-if (stl:begin v) (stl:end v) #'null) (stl:end v))
さて、どうだろう? Effective STL に記載されているコードとまったく別の書き方をするというのは今回が初めてではないかと思う。まぁこういうのを洗い出して記録していくのもこのシリーズの目的だと思う。地道に続けていくとしよう。
このシリーズのこれまでのエントリ
- 2015-09-19 - 第47項:書き込み専用コードの作成は避けよう
- 2015-09-20 - 第9項:消去オプションは注意して選択しよう
- 2015-09-22 - 第9項:消去オプションは注意して選択しよう - 2
- 2015-09-28 - 第9項:消去オプションは注意して選択しよう - 3
- 2015-09-30 - 第14項:reserve を使って不必要な割り当てを避けよう
- 2015-10-01 - 第17項:余分な容量を取り除くには「swap技法」を使おう
- 2015-10-09 - 第27項:コンテナのconst_iteratorをiteratorに変換するには、distanceとadvanceを使おう
- 2015-10-12 - 第5項:単一要素メンバ関数より範囲メンバ関数を使おう
- 2015-10-14 - 第30項:出力先範囲の大きさを確認しよう
- 2015-10-16 - 第31項:ソートの選択肢を知っておこう
- 2015-10-17 - 第31項:ソートの選択肢を知っておこう - 2
- 2015-10-23 - 第32項:本当に削除したい場合は、remove風アルゴリズムの後にeraseを使おう
- 2015-10-25 - 第33項:ポインタのコンテナには注意してremove風アルゴリズムを使おう
コメント
このページのタグ
Page tag : STLとその移植
Page tag : Common Lisp
Copyright(C) 2005-2021 project-enigma.
Generated by CL-PREFAB.