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ほにゃらら()で作ったオブジェクトがコピーされるときに、大量のインデックスがコピーされてしまう可能性がある。
この問題を解決するには、以下の方法が思いつく。

  1. Saのprivateなコンストラクタで好き勝手やって、fromほにゃらら()はinlineにすることで、戻り値最適化を狙う。
  2. fromほにゃらら()がポインタを返す。
  3. 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タイプの差になる

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。