OOPと参照ベースの相性の良さ
今週も、「SA作ってみよー」の流れで行ってる。
今はドキュメントを元にSAをメモリ上に構築して、そこから検索しているんだけど、もうそろそろSAを保存してあとから使いたい。
で、Javaならクラスメソッドで
class Sa { public static Sa fromDocument(InputStream doc) { // ドキュメントを読み込んでSAを構築 } public void saveToFile(string saveFilePath) { // ファイルに保存. } public static Sa fromSaved(string savedFilePath) { // saveToFile()で保存したファイルを読み込む. } }
とかすればいい。
さて、Saクラスがprivateメンバとして数十MBのインデックスを持っているとき、値ベースが基本になっているC++でこれをそのままやると、fromほにゃらら()で作ったオブジェクトがコピーされるときに、大量のインデックスがコピーされてしまう可能性がある。
この問題を解決するには、以下の方法が思いつく。
- Saのprivateなコンストラクタで好き勝手やって、fromほにゃらら()はinlineにすることで、戻り値最適化を狙う。
- fromほにゃらら()がポインタを返す。
- Sa自体を、コピーが軽量になるように設計する。
(1)の意味がわからない人は、「C++ 一時オブジェクト 最適化」あたりのキーワードで調べたら判ると思う。とりあえず見つけたページはコレ(http://www.asahi-net.or.jp/~uc3k-ymd/Lesson/Section03/section03_12.html)
で、(1)の欠点は、コンパイラの最適化機能に強く依存していることと、結局Saの最初の構築以外で発生するコピーには全く無力なところ。
(2)の欠点は、使用者がいちいち"->"とタイプしないといけなくて、"."とタイプするよりも面倒*1なことと、オブジェクトの削除方法が判らないこと。
前者は置いておくとして、後者は問題だ。作成者がnewでメモリを確保したという保証がないと、利用者は安心してdeleteできない。さらに、ダイナミックリンクを利用する環境などでは、作成者のnew/deleteと、利用者のnew/deleteが違う可能性が考えられる。new/deleteはただの演算子なので、コンパイラやlibc等が違うと対応が崩れてしまうかもしれない。
この問題に対処するには、Saのクラスメソッドにbuilderと対応するdestroyerを用意して、「メモリの確保と開放は同じレイヤで行う事」ルールを、設計者と利用者両方で守るのが、お行儀の良い方法だと思う。
生のポインタの代わりに破壊まで面倒を見るスマートポインタを返すという手もあるが、スマートポインタがtemplateで実装されていると、結局new/deleteの対応が取れない問題が発生する可能性があるので、注意が必要。
(3)は、Saがメンバ変数を値ではなくポインタで持ち、コピーコンストラクタと代入演算子を定義することで、copy on writeパターンもしくはimmutableパターンを使う方法。この方法は、メンバ変数すべてをcopy on writeもしくはimmutableにできる場合、C++のpImplパターンと非常に相性が良い。今回はこの方法を使おうと思う。
ただ、Javaのエスケープ解析を知ってからは、C++の値ベースってどうなんだろう・・・と思ってしまう。
*1:Shiftキーを入れると、1タイプと3タイプの差になる