静山とは墳墓の地の意.これこそまさにCDD(制御された解体)が希求しているところだ.いつ,どこで削除されてもきちんと始末が付けられることがCDDの目標である.前に「構造物を解体するのは簡単だ」と述べた.構造物はグラフ理論的には木であり,終端の葉から削除することによって秩序正しく解体できる.実はゼルコバの木で採用しているデータ構造にはこのような骨格としての木が埋め込まれている.つまり,ゼルコバの木システムはCOUPLINGオブジェクトを頂点とする木を構成しており,すべてのオブジェクトはこの木の一部である.プログラム開発の進化という観点から見ると,我々のプログラムは少なくとも「脊椎動物」の段階に達したと言えるだろう.逆にこのような骨格を持たないシステムは「軟体動物」の段階に留まっていると言えるかもしれない.
我々のシステムが元々木構造を取っているのなら,解体はもっと簡単にできてもよいのではないか?確かにその通りだが,その基礎の上にさまざまな系統の経路(血流系,呼吸系,リンパ系,神経系,ホルモン系,消化系,排泄系,運動系,その他もろもろ)が多重ネットワークを構成しているためことはそう簡単にはゆかない.現行でもあるオブジェクトを削除するとそれに接続している下位オブジェクトはすべて自動的に削除されるようになっているから,システム全体を破棄するためには,頂点のCOUPLINGを削除するだけでことは済むようになっているし,実際,現行でも最終的にはCOUPLINGの削除で止めを刺すようにはなっている.従って,CDDが完成するということは,現在COUPLING削除の前段で行っているすべての前処理が不要になることであると考えられる.
▲アプリ終了→…EraseTreeViewでGENEBOXを削除して被参照カウントの残留が出た GENELIST#34→GENEBOX#15223が残っている
このエラーにはSWO(SearchWrongObject)で仕掛けている検査ルーチンの動作が関わっている.snum=34というオブジェクトを追いかけているのだが,そこで使っているLIST::top()という関数が副作用を持っているためだ.topはリストの先頭要素を返すルーチンだが,curnod(カレントリスト要素)を更新するような動作になっている.昨日はcurnod自体を廃止しようとしていたのだが,一部で使われているため(直ちには)廃止することができなかった.top()をtoplistに変えることはできると思うのでやってみよう.⇒うまくいった.
▲アプリ終了の出口近くで,残留したLIST派生クラスのインスタンス64個のうち1個でデータ数不一致が発生している #34GENELISTでデータ数13のところゼロになっている ⇒ toplistはすでに空になっているので,datacount 不整合と考えられる.この原因はGENELISTではまだCDDをサポートしていないためと考えられる.⇒対処した.
現在実装されているLISTの派生クラスにはNLISTとnlistがある.いずれもテンプレートクラスでその下には以下の派生クラスがある.
- GENELIST GENEBOX ◯
- PAIRLIST PAIRBOX ◯
- NODELIST SIMPLENODE
- EDGELIST SIMPLEEDGE ◯
- TRIBELIST TRIBEBOX ◯
- COMPLIST REFLINK
最後のCOMPLISTはnlistの派生クラスで,それ以外はすべてNLISTだ.これらのうちの4つはすでに対応済みになっているので残り2つも片付けてしまおう.修正はただ単に各リスト要素クラスのデストラクタからdataCountDownを呼び出すというだけだから,あっと言う間に終わってしまう.~クラス名のデストラクタとその中から呼び出されるDisposeの切り分けはややあいまいだが,基本的にDisposeでは参照解決を行い,dataCountDownとCleaSlotだけはデストラクタで行うということにしておこう.デストラクタの入口では重複実行を回避するために if (finished()) return; としているが,これは完全に削除されたか,ないし~NODULEの処理が完了したことを意味しているので,重複実行される可能性はゼロではない.個別のデストラクタではこれと別に入口で disposing++; を実行しているので,これも見ることにする.
いや,これだけでは手抜きかもしれない.安全のため,そのリストに自ノードが接続していることを確認してからdataCountDownすることにしよう.COMPLISTは少し厄介だ.このリストは何をリスト要素としているのだろう?なるほど,なぜCOMPLISTがnlistでNLISTではないのか?ということが分かった.リスト構造は接続によって構成されるが,接続するスロットがゼロスロット固定なので,複数のリストに同時に連結することはできない.NLISTはオブジェクトを直接接続して構成されるリストだが,nlistはリスト要素クラスオブジェクトという特殊オブジェクトを要素とし,そこから実際のオブジェクトを参照するようになっているはずだ.COMPLISTは以下のように定義されている.
class COMPLIST : public nlist<SIMPLENODE>
いや,ちょっと違うかもしれない.nlistの定義を見ると,
nlist 基本オブジェクトリスト(リスト要素はnoduleの派生クラスオブジェクト)
となっている.NLISTとnlistの違いはなんだろう?NLISTの説明もまったく同じだ.
NLIST 実装オブジェクトリストクラス(リスト要素はnoduleの派生クラスオブジェクト)
ただし,テンプレートが少し違う.
template <class NODECLASS> — NLIST
template <class LISTNODE, int CID> — nlist
いや,それだけではない.nlistではSLOTZEROではなくSLOTONEを使っている.しかし,オブジェクトは参照でなく接続として生成されている.ただし,そのオブジェクトは<NODECLASS>で指定したオブジェクトではなく,REFLINKという特殊オブジェクトだ.REFLINKはサイズ1の配列クラスとして定義されている.上のSLOTONEというのはREFLINKからオブジェクトを参照するためのスロットだ.nlistの定義というより,説明文は間違っているか,少なくともあまり正確ではない.
nlist 基本オブジェクトリスト(リスト要素REFLINKからnoduleの派生クラスオブジェクトを参照)
としておこう.従って,COMPLISTではREFLINKからdataCountDownが送られることになる.⇒これで6つでそろった.これですべてだろうか?LISTから直接生成されているインスタンスはなかったろうか?ないという訳ではない.というか,存在する.たとえば,参照リストなどはそのタイプだ.系列枠のスプリット検定用リストなどというものもある.シンメトリ婚スプリット検定用リストというのもこのタイプだ.DATALISTは2つのタイプのリストを生成することができる.一つは前出したREFLINKをリスト要素とするもの,もう一つのタイプは一般オブジェクトクラスのnoduleを要素とするものだ.REFLINKではすでに整備されているが,noduleにも同様措置を取る必要がある.
noduleクラスにはリスト検索ルーチンとしてgetlistというのがある.これはノード型という属性をチェックして所属リストか否かを判定しているが,ここではこれらの属性を無視してより一般的なgetUpperClassNode(‘h’)で検索することにする.これで少なくともリスト要素に関しては万全整ったのではないかと思われる.
参照カウントの不一致が出てしまった.多分,~noduleで親リストを探すのに一般的手法を使ったためではないかと思われる.終末処理ではnoduleのデストラクタは最後に呼び出されるから,それ以前にすでに処理が済んでいるものを重ねて処理することになってしまっているのではないか?これを回避するためにはやはり,旧来ロジックのような属性チェックが必要ということだろう.⇒どうもまだ通っていない.
▲ZTシステム構成図7.ZELを起動してSIMPLEGRAPH:DecompConnectedComponentの冒頭でCOMPLISTリストのキャンセルを実行しているところでnodule::operator deleteが(nptr->refcnt && PHASE != CLEARTABLE)というエラーを出している このリストはCOMPLIST#143473で要素数40個,cancelの冒頭ではパラメータは整合している cancel動作そのものはDATALIST::cancellにゆだねているが,事後Bottomlist(NULL)を実行して参照解除している
多分これは以前はDATALIST::cancellの前に実行していたのではないかと思う.bottomlistやtoplistの健全性はつねに検査されているので,事前にセットするのはやはり好ましくない.この参照削除に責任があるのはリスト要素であるREFLINKそのものと考えられるが,REFLILNK:Disposeではなにもやっていない.dataCountDownをDisposeに移動し,合わせてLIST::ClearSansyoを実行するようにして解決した.ただし,dataCountDownではリストのパラメータの調整を行っているのだから,本来ならClearSansyoを実行しなくても動作するはずである.そうなっていないのは,LIST::dataCountDownに問題があるからだ.
実際の削除を実行しているのはDATALISTだが,LIST:dataCountDownではLISTクラスのメンバ変数についての操作しか実行していない.これでは明らかに不十分だ.現行ではdataCountDownという関数はLISTクラスに唯一存在し,それだけですべてをカバーするようになっている.もし,それを仕様とするのであれば,LIST:dataCountDownですべてのことをやらなくてはならない.しかし,それでは基本クラス以下が何をやっているかすべて知らなくてはならないので,現実的ではない.むしろ,LIST::dataCountDownの中から下位クラスに通知をパスして,いまの場合で言えばDATALIST:dataCountDownを実行するようにさせるべきだろう.そうしておかないと,今後さらに拡張したときに問題が生じるおそれがある.
実行順序に関して言えば,デストラクタの実行順序とは逆に下位クラス(基本クラス)のdataCountDownを先に実行した方がよいのではないかという気がする.⇒いや,素直にデストラクタの実行順序を踏んだ方が安全なのではないか?⇒それはそれでよいことにしよう.原則的にはすべてのクラスが固有の遺言執行書を書く必要がある.遺言執行書がこように階層化されるとすれば,この関数は仮想化すべきなのではないだろうか?さもないとどこかで中抜きされて下位クラスに渡されてしまう可能性がある.修正してみよう.
いや,そんな簡単な話ではないのではないか?noduleのデストラクタで遺言書を送信したときのことを考えた方がよい.noduleの生のインスタンスがリスト要素になる場合があるから,noduleには遺言書を発行する資格がある.しかし,たとえばSIMPLENODEの中にはnoduleが入っているからSIMPLENODEとnoduleが同時に遺言書を発行したら,処理がかち合ったり,ダブったりする可能性はないか?対象を直列リストに限定したとしても,リストクラスはそれ自体が階層になっているから,各クラスの処理にダブりがない限り,各クラス別に遺言書を出すことも合理化できるとしよう.リストクラスがNあるとすれば,N個の異なる遺言書のテンプレートがあることになる.これはいいだろう.
問題はこれを発行する側にある.つまり,オブジェクト自身クラス階層の上にあり,デストラクタはこの階層を下降して逐次処理を行うという仕掛けになっている.つまり,あるオブジェクトは玉ねぎのようにそれ自体階層化している.しかし,上位ですでに遺言書を発行していることはblackflag をチェックすれば確認できるはずだ.従って,noduleで起きた問題は検査を厳格にしなくても,フラグをチェックするだけで回避できた可能性がある.⇒まず,この点について確認してみよう.ロジックを元に戻してエラーが発生することは確認できた.⇒いや,このエラーももう少し詳しく見る必要がある.
エラーは参照リスト管理の中で起きている.この中ではリスト要素の削除が実行されるので,内部的に参照リンク操作が含まれている.リスト要素はnoduleなのでnoduleからCDCが発行され,~nodule→ find→ top→ SansyoでReferenceControlの不一致が発生している.⇒何とか強引に動かした.ポイントは「参照リスト管理」の中では参照を操作してはならないという点だ.いや,実際そういう作りにはなっているのだが,remove関数の中でデバッグ用に実行している検査関数が副作用を与える点が問題だ.top()やcurnodを存続させるとしても,検査用に別途副作用のないルーチンを確保しなくてはならない.
どうも先にそれをやらないとあちこちで躓いてしまうのではないか?引数を持たないnext()などはcurnodを基準としているので,それがないと都合の悪いことが起きる.最近は引数なしのnextを使った記憶はほとんどない…いや,それほどは使われていない.全体で5箇所だけだ.これは真っ先に廃止してよいだろう.Next(void)というのもある.curnodに関してはベタ参照にしてしまうという手もないわけではない.あまりよい考えではないかもしれないが… なぜだろう?nlistは引数付きのNextを持っていない.⇒Followingという名前を使っていた.
ついでにtopとTopも廃止したいところだが,その前にバックアップ.top()はさすがに多い.303箇所もある.curnodは50箇所の出現する.ただし,そのうちの10箇所は「curnodを廃止する」だ.⇒curnodの廃止はひとまず置いて,先にいま停止しているところをクリアしてしまおう.上記で解析した通り,この問題を解決するためにはDATALIST:dataCountDownを作るしかない.というかそれが最善策だと思う.方針を立ててみよう.
- 現在の「制御された解体」の範囲を直列リスト操作に限定する
- 各リストクラスは個別に事後処理関数dataCountDownを持つ
- ただし,削除そのものを実行していないクラスでは持たなくてもよい
- 削除対象となるリスト要素のデストラクタでは自ノードが接続するリストを検索し,存在すればdataCountDownを送信する
- 親リストとのコンタクトが切れている場合には送信しない(できない)
- また,自ノードのblackflagがオンの場合も送信しない(送付済み)
- dataCountDown関数では入口でblackflagをインクリメントする
- dataCountDown関数では出口で下位のdataCountDownを呼び出す
- 削除後の後処理としてdataCountDownを実行する
- ただし,対象リスト要素のblackflagがオンの場合には実行しない
- 処理完了後にblackflagをリセットしない
現時点ではLIST::dataCountDownしかないので,追加してインプリメントされるのはDATALIST::dataCountDownだけだ.この関数では自クラスが管理するtoplistの更新を行う.これは現在 delete を実行しているブロックのロジックをそっくりdataCountDown関数の中に移植するだけのはずだ.dataCountDown関数は内部操作の場合と外部で削除された場合の共通論理となる.
一つのクラスでdeleteを実行している箇所が複数ある場合にはどうなるか,それぞれに異なる遺言書を付けるのか?⇒そのようなことはあり得ない.もし,現行でそういう論理が存在するとすれば,deleteを実行しているブロックを関数化して一箇所にまとめるべきだ.そうできないという理由は考えられない.もちろん遺言執行書は一クラスで高々一つ持つだけだ.DATALISTにはdeleteを実行している箇所が3箇所あるので,deleteElementとしてまとめよう.LISTは1箇所だけなのでその必要はないが,同じ名前の関数を作って分離することにする.