Copyright 1999 Rogue Wave Software
Copyright 1999 Sun Microsystems, Inc.
実装に関する注意事項 |
17 |
動的リンクライブラリ
Tools.h++ クラスライブラリは、Microsoft Windows 3.X の動的リンクライブラリ (DLL) としてリンクすることができます。DLL では、リンクは、ルーチンが実際に使用される実行時に起こります。この結果、ルーチンは必要な場合だけに獲得されるので、実行可能ファイルは小さくなります。その他にも、多くのアプリケーションで同じコードを共有できるので、アプリケーションごとに同じコードを重複して作成しなくてもかまわないという利点もあります。
Windows と C++ の技術の進歩は非常に速いので、必ず配布ディスク上のファイル TOOLDLL.DOC を見て、更新がないかをチェックしてください。
DLL の例
この節では、Tools.h++ の DLL を使用する Windows のサンプルアプリケーション DEMO.CPP について説明します。このプログラムのコードは、サブディレクトリ DLLDEMO に入っています。このプログラムは、よく知られているカテゴリである「Hello World」の例に分類されます。
/* * Tools.h++ の DLL を使用する Windows 3.X のサンプルプログラム */ #include "demowind.h" int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR , int nCmdShow) // 1 { // 「DemoWindow」クラスのインスタンスを作成する DemoWindow ww(hInstance, hPrevInstance, nCmdShow); // 2 // それにいくつかの項目を追加する ww.insert( new RWRectangle(200, 50, 250, 100)); // 3 ww.insert( new RWEllipse(50, 50, 100, 100)); ww.insert( new RWText(20, 20, "Hello world, from Rogue Wave!")); ww.show(); // ウィンドウを表示する // 4 ww.update(); // ウィンドウを更新する // メッセージループに入る MSG msg; while( GetMessage( &msg, NULL, 0, 0)) // 5 { TranslateMessage( &msg ); DispatchMessage( &msg ); } return msg.wParam; // 6 } |
プログラムの各行について説明します。
//1 | すべての Windows プログラムに必要な Windows プログラムのエントリポイントです。これは、C の main 関数に相当します。 |
//2 | クラス DemoWindow のインスタンスを作成します (後述)。これは、オブジェクトを挿入できる抽象ウィンドウを表します。 |
//3 | 長方形、楕円、テキスト文字列の 3 種類のオブジェクトを DemoWindow に挿入します。 |
//4 | DemoWindow に対して、それ自体を表示し、それ自体を更新するよう指示します。 |
//5 | 最後に、Windows のメインイベントループに入ります。 |
//6 | DemoWindow のデストラクタが、そのウィンドウオブジェクトに割り当てられていたメモリーすべてを解放します。 |
メンバー関数 paint() は、ウィンドウを再描画するときに呼び出すことができます。リスト myList が調べられて、その各項目が画面上に再描画されます。
次に、DEMOWIND.H ファイルのリストを示します。
#ifndef __DEMOWIND_H__ #define __DEMOWIND_H__ /* * デモンストレーションウィンドウクラス --- これを使用すると、 * 基底クラス RWDrawable から継承する項目をそれに「挿入」 * することができる */ #include <windows.h> #include <rw/slistcol.h> #include "shapes.h" class DemoWindow { HWND hWnd; // 自分のウィンドウのハンドル HINSTANCE myInstance; // 自分のインスタンスのハンドル int nCmdShow; RWSlistCollectables myList; // ウィンドウ内の項目のリスト public: DemoWindow(HINSTANCE mom, HINSTANCE prev, int); ~DemoWindow(); void insert(RWDrawable*); // 新しい項目をウィンドウに追加する HWND handle() {return hWnd;} int registerClass(); // 最初の登録 void paint(); // Windows の手続きによって呼び出される int show() {return ShowWindow(hWnd,nCmdShow);} void update() {UpdateWindow(hWnd);} friend long FAR PASCAL _export DemoWindow_Callback(HWND, UINT, WPARAM, LPARAM); }; DemoWindow* RWGetWindowPtr(HWND h); void RWSetWindowPtr(HWND h, DemoWindow* p); #endif |
#include <windows.h> #include <rw/vstream.h> #include "demowind.h" #include "shapes.h" #include <stdlib.h> #include <string.h> /* * 新しいウィンドウを作成する */ DemoWindow::DemoWindow( HINSTANCE mom, HINSTANCE prev, int cmdShow) //1 { myInstance = mom; nCmdShow = cmdShow; // 前のインスタンスがない場合はクラスを登録する if(!prev) registerClass(); //2 hWnd = CreateWindow("DemoWindow", //3 "DemoWindow", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, myInstance, (LPSTR)this ); // 「this」を隠す //4 if(!hWnd) exit( FALSE ); } /* *自分自身を登録する。1 回だけ呼び出される */ int DemoWindow::registerClass(){ //5 WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = ::DemoWindow_Callback; //6 wndclass.cbClsExtra = 0; // 「this」ポインタを格納するための追加のスペースを要求する wndclass.cbWndExtra = sizeof(this); //7 wndclass.hInstance = myInstance; wndclass.hIcon = 0; wndclass.hCursor = LoadCursor( NULL, IDC_ARROW ); wndclass.hbrBackground = GetStockObject( WHITE_BRUSH ); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = "DemoWindow"; if( !RegisterClass(&wndclass) ) exit(FALSE); return TRUE; } DemoWindow::~DemoWindow(){ // myList 内のすべての項目を削除する myList.clearAndDestroy(); //8 } void DemoWindow::insert(RWDrawable* d){ // 新しい項目をウィンドウに追加する myList.insert(d); //9 } void DemoWindow::paint(){ //10 RWDrawable* shape; PAINTSTRUCT ps; BeginPaint( handle(), &ps); //11 // リスト内のすべての項目を描画する。反復子の作成から開始する RWSlistCollectablesIterator next(myList); //12 // コレクションを反復して、各項目を描画する while( shape = (RWDrawable*)next() ) //13 shape->drawWith(ps.hdc); //14 EndPaint( handle(), &ps ); //15 } /* * このウィンドウクラスのコールバックルーチン */ long FAR PASCAL _export DemoWindow_Callback(HWND hWnd, unsigned iMessage, //16 WPARAM wParam, LPARAM lParam) { DemoWindow* pDemoWindow; if( iMessage==WM_CREATE ){ //17 // 「this」ポインタを作成した構造体から獲得して、それを // ウィンドウインスタンスに挿入する LPCREATESTRUCT lpcs = (LPCREATESTRUCT)lParam; pDemoWindow = (DemoWindow*) lpcs->lpCreateParams; RWSetWindowPtr(hWnd, pDemoWindow); return NULL; } // 適切な「this」ポインタを獲得する pDemoWindow = RWGetWindowPtr(hWnd); //18 switch( iMessage ){ case WM_PAINT: pDemoWindow->paint(); //19 break; case WM_DESTROY: PostQuitMessage( 0 ); break; default: return DefWindowProc(hWnd, iMessage, wParam, lParam); }; return NULL; } void RWSetWindowPtr(HWND h, DemoWindow* p){ //20 SetWindowLong(h, 0, (LONG)p); } DemoWindow* RWGetWindowPtr(HWND h){ //21 return (DemoWindow*)GetWindowLong(h, 0); } |
//1 | DemoWindow のコンストラクタ。これは、それを作成するアプリケーションインスタンスのハンドル mom、既存のインスタンスのハンドル prev、ウィンドウをアイコン形式で表示するかどうかの指示を必要とします。これらの変数はメインウィンドウ手続きである WinMain から受け取られます (前述)。 |
//2 | コンストラクタは、前のアプリケーションインスタンスが実行されているかどうかを確認し、実行されていない場合はクラスを登録します。 |
//3 | 新しいウィンドウが作成されます。 |
//4 | 主な機能は、Windows の追加データ機能を使用したこの DemoWindow の this ポインタの格納です。この手続きは少し厄介ですが、非常に便利です。ここに入れられる値は、CREATESTRUCT 構造に示されます。これは、CreateWindow 関数によって生成される WM_CREATE メッセージを処理するときに検索することができます。 |
//5 | このメンバー関数は、アプリケーションの最初のインスタンスのためだけに呼び出されます。 |
//6 | 大域関数 DemoWindow_Callback は、このウィンドウのコールバック手続きとして登録されます。 |
//7 | Windows に対して、この Windows クラスの this ポインタ用スペースを取っておくように要求します。これは、Windows のハンドルを特定の DemoWindow に関連付けるために使用されます。 |
//8 | デストラクタは clearAndDestroy() を呼び出して、myList に挿入されたすべての項目を削除します。 |
//9 | メンバー関数 inseret(RWDrawable*) は、新しい項目をウィンドウに挿入します。RWDrawable は RWCollectable から継承するので、RWSlistCollectables::insert(RWCollectable*) を呼び出すときにキャストを実行する必要はありません (431 ページの「SHAPES.H からの抜粋」を参照)。myList を非公開にして、型 RWDrawable* の引数だけをとる、制限された挿入を行うことにより、drawable だけがウィンドウに挿入されるようにすることができます。 |
//10 | これは描画手続きであり、ウィンドウの再描画時に呼び出されます。 |
//11 | このウィンドウのハンドルを獲得することから開始して、BeginPaint を呼び出し、描画に関する情報によって PAINSTRUCT を埋めます。 |
//12 | 描画される項目のリストを調べる準備として反復子が作成されます。 |
//13 | 次に描画される項目を獲得します。NULL が返された場合は、それ以上項目がないので、while ループは終了します。 |
//14 | 各項目に対して、仮想関数 drawWith を呼び出して、項目そのものを指定のデバイスコンテキスト上に描画させます。 |
//15 | PAINSTRUCT が返されます。 |
//16 | これは、このウィンドウのメッセージを処理する必要があるときに呼び出されるコールバックルーチンです。このルーチンは、非常に便利な Borland C++ の _export キーワードを呼び出して、この関数をエクスポートする必要があることを示します。この手続きを使用すると、モジュール定義ファイルの Exports セクションに、関数をリストする必要がなくなります。 |
//17 | メッセージが WM_CREATE メッセージの場合、それは CreateWindow 関数によって生成されており、この手続きで最初のことです。CREATESTRUCT から this ポインタをフェッチして (行 4 に挿入されている)、それを Windows の追加データの挿入するには、かなり技巧的な手続きを使用してください。関数 RWSetWindowPtr については後で定義します。 |
//18 | 関数 RWGetWindowPtr(HWND) は、Windows の HANDLE を指定して、適切な DemoWindow へのポインタを検索するために使用されます。 |
//19 | WM_PAINT が検索された場合、前述の paint() メンバー関数を呼び出します。 |
//20 | この関数は、this ポインタを Windows の追加データに挿入するために使用されます。これは、Windows のハンドルを DemoWindow のインスタンスに 1 対 1 対応させるという考え方です。 |
//21 | この関数は、this ポインタをフェッチしてバックアウトするために使用されます。 |
SHAPES.H からの抜粋
この節では、RWDrawable のサブクラスについて説明します。クラス RWDrawable は、Tools.h++ クラス RWCollectable から継承する抽象基底クラスであり、新しいメンバー関数 drawWith(HDC) を追加します。RWDrawable を特殊化するサブクラスは、この関数を実装して、それ自体を指定のデバイスコンテキストハンドル上に描画します。
ここでは、RWRectangle というサブクラスを 1 つだけをもう一度示します。クラス RWRectangle は、RWDrawable から継承します。これは、Windows から提供された構造 RECT を、長方形の角を保持するためのメンバーデータとして使用します。
class RWDrawable : public RWCollectable{ public: virtual void drawWith(HDC) const = 0; }; class RWRectangle : public RWDrawable{ RWDECLARE_COLLECTABLE(RWRectangle) public: RWRectangle() { } RWRectangle(int, int, int, int); // RWDrawable から継承 virtual void drawWith(HDC) const; // RWCollectable から継承 virtual Rwspace binaryStoreSize() const {return 4*sizeof(int);} virtual unsigned hash() const; virtual RWBoolean isEqual(const RWCollectable*) const; virtual void restoreGuts(RWvistream& s); virtual void restoreGuts(RWFile&); virtual void saveGuts(RWvostream& s) const; virtual void saveGuts(RWFile&) const; private: RECT bounds; // 長方形の境界 }; |
#include "shapes.h" #include <rw/vstream.h> #include <rw/rwfile.h> RWDEFINE_COLLECTABLE(RWRectangle, 0x1000) //1 // コンストラクタ RWRectangle::RWRectangle(int l, int t, int r, int b) //2 { bounds.left = l; bounds.top = t; bounds.right = r; bounds.bottom = b; } // Drawable から継承 void RWRectangle::drawWith(HDC hdc) const //3 { // Windows 呼び出しを行う Rectangle(hdc, bounds.left, bounds.top, bounds.right, bounds.bottom); } // RWCollectable から継承 unsigned RWRectangle::hash() const //4 { return bounds.left ^ bounds.top ^ bounds.bottom ^ bounds.right; } RWBoolean RWRectangle::isEqual(const RWCollectable* c) const //5 { if(c->isA() != isA() ) return FALSE; const RWRectangle* r = (const RWRectangle*)c; return bounds.left == r->bounds.left && bounds.top == r->bounds.top && bounds.right == r->bounds.right && bounds.bottom == r->bounds.bottom; } // 仮想ストリームから RWRectangle を復元 void RWRectangle::restoreGuts(RWvistream& s) //6 { s >> bounds.left >> bounds.top; s >> bounds.right >> bounds.bottom; } // RWFile から復元 void RWRectangle::restoreGuts(RWFile& f) //7 { f.Read(bounds.left); f.Read(bounds.top); f.Read(bounds.right); f.Read(bounds.bottom); } void RWRectangle::saveGuts(RWvostream& s) const //8 { s << bounds.left << bounds.top; s << bounds.right << bounds.bottom; } void RWRectangle::saveGuts(RWFile& f) const //9 { f.Write(bounds.left); f.Write(bounds.top); f.Write(bounds.right); f.Write(bounds.bottom); } |
//1 | RWCollectable のすべてのサブクラスを 1 回だけコンパイルするために必要なマクロ。詳細については、328 ページの「RWCollectable オブジェクトの作成方法」を参照してください。 |
//2 | RECT 構造体を埋める RWRectangle のコンストラクタ。 |
//3 | 仮想関数 drawWith(HDC) の定義。これは、適切な引数を付けて、Windows 関数 Rectangle() への呼び出しだけを行います。 |
//4 | この RWRectangle をハッシュテーブルから検索する必要がある場合に、適切なハッシュ値を提供します。 |
//5 | 適切な isEqual() 関数の実装を提供します。 |
//6 | この関数は、仮想ストリームから RWRectangle を復元します。 |
//7 | この関数は、RWFile から RWRectangle を復元します。 |
//8 | この関数は、仮想ストリームに RWRectangle を格納します。どうして、空白によって要素を区切る必要がないかに注意してください。これは、必要に応じて仮想ストリームによって行われます。 |
//9 | この関数は、RWRectangle を RWFile に格納します。 |
他の形状 RWEllipse と RWtext も同様の方法で実装されます。
書き込み時のコピー
クラス RWCString、RWWString、RWTValVirtualArray<T> は、コピーを最小化するため「コピー・オン・ライト (書き込み時のみコピーする)」という技法を使用します。この技法は、わかりやすい値にもとづく意味、考え方にしたがったもので、参照回数をカウントするポインタを実装し非常に高速です。
RWCString(const RWCString&);
次の例では、4 つのオブジェクトの内 1 つが文字列を変更するまで、4 つのオブジェクトが文字列の 1 つのコピーを共有する場合を示します。
#include <rw/cstring.h> RWCString g; // Global object void setGlobal(RWCString x) { g = x; } main(){ RWCString a("kernel"); // 1 RWCString b(a); // 2 RWCString c(a); // 3 setGlobal(a); // "kernel" のコピーは 1 回のままである // 4 b += "s"; // ここで b がそれ自身のデータ "kernels" を持つ // 5 } |
//1 | 文字列 "kernel" を保存するためのメモリーの実際の割り当てと初期化が行われるところが RWCString オブジェクト "a" です。 |
//2-//3 | そこからオブジェクト "b" および "c" が作成されると、元のデータの参照カウントが増分されるだけで返ります。この時点では、同じデータを参照するオブジェクトが 3 つ存在します。 |
//4 | 関数 setGlobal() が大域的な RWCString g の値を同じ値に設定します。ここで参照カウントが 4 になりますが、文字列 "kernel" のコピーはまだ 1 つだけです。 |
//5 | 最後に、オブジェクト "b" がこの文字列の値の変更を試みます。このとき、参照カウントをチェックし、それが 1 より大きいか調べます。1 より大きいということは、この文字列が複数のオブジェクトによって共有されていることを意味します。この時点で文字列の複製が作成および修正されます。元の文字列の参照カウントが 3 になり、新しく作成された複製文字列の参照カウントが 1 になります。 |
例
RWCString のコピーは非常に効率が良いので、それらをオブジェクト内の値によって格納する (ポインタを格納するのでなく) という方法がとりやすくなります。次に示す比較でわかるように、これによって管理は大幅に単純化されます。たとえば、いま 1 つウィンドウがあって、その背景色と前景色を設定できるものとします。これを行うための 1 つの単純な方法は、次に示すようなポインタの使用です。
class SimpleMinded { const RWCString* foreground; const RWCString* background; public: setForeground(const RWCString* c) {foreground=c;} setBackground(const RWCString* c) {background=c;} };
以下に、もっと簡単な方法を示します。
class Smart { RWCString foreground; RWCString background; public: setForeground(const RWCString& c) {foreground=c;} setBackground(const RWCString& c) {background=c;} };
Smart window; RWCString color("white"); window.setForeground(color); // white への 2 つの参照 color = "Blue"; // white への 1 つの参照。blue への 1 つの参照
RWStringID は、Tools.h++ バージョン 7 以降の RWCollectables の識別子です。これは、RWCString から派生し、どの const RWCString メソッドでも操作することができます。クラスの RWStringID が実行時に変更された場合に起こる可能性のある障害を防止するために、非 const メソッドは隠されています。
RWStringID は、2 つの方法のどちらかによって、RWCollectable クラスと関連付けることができます。つまり、クラスの RWStringID を自分で指定する方法か、あるいはクラスの名前そのままの文字列である RWStringID をライブラリに自動的に生成させる方法のどちらかです。たとえば、class MyColl:public RWCollectable は、自動的に RWStringID 「MyColl」を獲得します。
マクロ RWDEFINE_COLLECTABLE を次のように使用すると、固定 RWClassId と、生成される RWStringID を持つクラスを指定することができます。
RWDEFINE_COLLECTABLE(ClassName, ClassID) RWDEFINE_COLLECTABLE(MyCollectable1,0x1000) //たとえば
RWDEFINE_NAMED_COLLECTABLE(ClassName, StringID) RWDEFINE_NAMED_COLLECTABLE(MyCollectable2, "Second Collectable") // たとえば
// まずは、実験を設定する MyCollectable1 one; MyCollectable2 two; // 実行中のすべての RWClassID が異なると保証される one.isA() != two.isA(); // すべての RWCollectable が RWStringID を持つ one.stringID() == "MyCollectable1"; // ID を検索する方法はいくつかある RWCollectable::stringID(0x1000) == "MyCollectable1"; two.isA() == RWCollectable::classID("Second Collectable");
RWStringID 識別子の追加によって、古い制約のもとで可能であったよりもさらに多くの識別子と、人間にわかりやすい RWCollectable 識別子が可能になります。新しい識別子を収容するために、開発者によって指定された RWStringID を持つ各 RWCollectable クラスに、一時的な RWClassID が生成されます。これらの RWClassID は、実行可能プログラムの実行中に必要に応じて作成され、その実行の間変更されません。しかし、実行可能プログラムが違っていたり、再度実行するときには、これらの ID は別の順序で生成される可能性があるので、永久記憶には適していません。
RWStringID を使用するプログラミング
RWCollectable には、新しい通常メンバー関数と 2 つの新しい静的メンバー関数が追加されました。Tools.h++ のバージョン 7 の主な目的の 1 つは、バージョン 6 でコンパイルされたオブジェクトとのリンク互換性を維持することであるので、これらの関数はどちらも仮想関数ではありません。したがって、これらの関数は、リンク互換性を考慮しない場合よりもやや非効率的です。
新しい通常メンバー関数は次のとおりです。
RWStringID stringID() const;
RWStringID stringID(RWClassID); // RWStringID を調べる RWClassID classID(RWStringID); // classID を調べる
void addFunction(RWuserCreator, RWClassID, RWStringID); RWCollectable* create(RWStringID) const; RWuserCreator getFunction(RWStringID) const; void removeFunction(RWStringID); RWStringID stringID(RWClassID) const; RWClassID classID(RWStringID) const;
RWCollectable *ctp; // ポインタを割り当てる if (ctp->isA() == SOME_CONST_CLASSID) // 特定の処理を実行する
RWCollectable *ctp; // ポインタを割り当てる // 比較のために、既存の RWCollectable を使用する // 比較は RWStringID の比較よりも速く処理される if(ctp->isA() == someRWCollectablePtr->isA()) // クラスインタフェースにコーディングできる // ... // 識別をハードコーディングする慣用法。文字列比較は // int 比較よりも遅いので、少しだけ遅くなる。 // stringID() も辞書検索を使用する。 if (ctp->stringID() == "Some ID String") { // クラスインタフェースにコーディングできる }
このことは、各クラスのオブジェクトの総数には何も影響しません。この数字は、各自のオペレーティングシステムとコンパイラの要件だけによって制限されます。当然、0x8000 以下の RWClassID のフルセット (さらに可能な 32767 個 の RWCollectable) にアクセスすることもできますが、これらは自動生成されません。これらは手作業で指定する必要があります。
静的メソッドによる仮想メソッドの実装
仮想メソッド isA() は「実行時の固有な」RWClassID を返すので、この仮想メソッドを使用して、各種のデータまたは関数ポインタが保存されるルックアップテーブルへのインデックスを提供することができます (C++ の組み込み vtables に似ています)。RWCollectables は、単一 RWFactory の存在にすでに依存しているので、この RWFactory インスタンスを使用して、ルックアップ情報を保持することを選択しました。
次の静的メソッドは、RWFactory インスタンス内の id を検索しようとします。
RWStringID RWCollectable::stringID(RWClassID id);
次の静的メソッドも同様の方法で動作して、RWFactory インスタンスを調べて、sid に関連する RWClassID があるかどうかを調べます。
RWClassID RWCollectable::classID(RWStringID sid)
同じ RWStringID を持つ RWCollectable を含むコレクションは、同じ RWCollectable への複数参照が初めて現れたときだけに保存されるように、その RWStringID をストリームまたはファイルに 1 回だけ保存することに注意してください。
しかし、衝突によって問題が生じる場合もあります。衝突するユーザー定義 RWStringID を持つクラスの多相持続は正常に動作しません。このような場合、データは正常に保存されていても、回復不能になります。同様に、RWStringID だけにもとづくクラスの区別に依存するユーザーコードも失敗します。
開発者は、このような衝突を避けることができます。まず、他と衝突することがない RWStringID を使用する必要があります。たとえば、ユーザーのクラスの継承階層を模倣する RWStringID を選択したり、あるいは、ソースファイル版管理システム (RCS 等) にあるような、ユーザーの名前、ユーザーの企業名、作成時刻、ファイルパスを組み込んだ、RWStringID を選択することができます。もちろん、プログラムを必ずテストして、RWStringID に実際に対応するクラスが予想どおりであることを確認する必要があります。
RWCollectable の格納と検索の詳細
290 ページの「演算子」では、次の大域関数を使用して、クラスの構造 (つまり、ポインタの関係) を保存して復元する方法を説明しました。
Rwvostream& operator<<(RWvostream&, const RWCollectable&); RWFile& operator<<(RWFile&, const RWCollectable&); Rwvostream& operator<<(RWvostream&, const RWCollectable*); RWFile& operator<<(RWFile&, const RWCollectable*); Rwvistream& operator>>(RWvistream&, RWCollectable&); RWFile& operator>>(RWFile&, RWCollectable&); Rwvistream& operator>>(RWvistream&, RWCollectable*&); RWFile& operator>>(RWFile&, RWCollectable*&);
任意のコレクタブルオブジェクトに対して初めて左シフト (<<) 演算子の 1 つを呼び出すと、同一性辞書が内部的に作成されます。このオブジェクトのアドレスが、出力ファイル内での順番 (最初、2 番目など) とともに辞書に入れられます。
これが実行されると、そのオブジェクトの仮想関数 saveGuts() への呼び出しが行われます。これは仮想関数であるので、その呼び出しは派生クラスによって使用される saveGuts() の定義に渡されます。前述のとおり、saveGuts() はオブジェクトの内部構成要素を格納します。そのオブジェクトに RWCollectable から継承する他のオブジェクトが含まれている場合は、そのオブジェクトの saveGuts() は、これらの各オブジェクトに対して operator<<() を再帰的に呼び出します。
operator<<() 以降の呼び出しは新しい 識別ディレクトリを作成せず、そのオブジェクトのアドレスをすでに存在する辞書に格納します。前に書き込まれたオブジェクトのアドレスと同一のアドレスが見つかった場合、saveGuts() は呼び出されません。その代わり、このオブジェクトが前の (たとえば、6 番目の) オブジェクトと同一であるという参照が書き込まれます。
コレクション全体が調べられて、saveGuts() への初期呼び出しが返ると、次に同一性辞書が削除されて operator<<() への初期呼び出しが返ります。
関数 operator>>() は、restoreGuts を呼び出してストリームまたはファイルからメモリにオブジェクトを復元することによって、書き込み作業全体を基本的に反転させます。この関数は、すでに作成されているオブジェクトへの参照を見つけた場合、RWFactory に新しいオブジェクトを作成するよう要求するのではなく、古いオブジェクトのアドレスを返します。
以下に、これらの機能を使用するクラスのより高度な例を示します。
#include <rw/collect.h> #include <rw/rwfile.h> #include <assert.h> class Tangle : public RWCollectable { public: RWDECLARE_COLLECTABLE(Tangle) Tangle* nextTangle; int someData; Tangle(Tangle* t = 0, int dat = 0){nextTangle=t; someData=dat;} virtual void saveGuts(RWFile&) const; virtual void restoreGuts(RWFile&); }; void Tangle::saveGuts(RWFile& file) const{ RWCollectable::saveGuts(file); // 基底クラスを保存する file.Write(someData); // 内部状態を保存する file << nextTangle; // 次のリンクを保存する } void Tangle::restoreGuts(RWFile& file){ RWCollectable::restoreGuts(file); // 基底クラスを復元する file.Read(someData); // 内部状態を復元する file >> nextTangle; // 次のリンクを復元する } // p で始まり NULL で終わるリストの完全性を検査する void checkList(Tangle* p){ int i=0; while (p) { assert(p->someData==i); p = p->nextTangle; i++; } } RWDEFINE_COLLECTABLE(Tangle, 100) main(){ Tangle *head = 0, *head2 = 0; for (int i=9; i >= 0; i--) head = new Tangle(head,i); checkList(head); // 元のリストを検査する { RWFile file("junk.dat"); file << head; } RWFile file2("junk.dat"); file2 >> head2; checkList(head2); // 復元したリストを検査する return 0; } |
クラス Tangle は (潜在的) 循環リンクリストを実装します。何が起こるでしょう。Tangle のインスタンスに対して初めて関数 operator<<() が呼び出されると、この関数は前述のように同一性辞書を設定し、それから上記の定義を持つ Tangle の saveGuts() を呼び出します。この定義は Tangle のすべてのメンバーデータを格納してから、次のリンクに対して operator<<() を呼び出します。この再帰がチェーン (chain) の中で繰り返されます。
そのチェーンが NULL オブジェクト (つまり、nextTangle がゼロ) で終わった場合、operator<<() はこれを内部的に記録し、再帰を停止させます。
一方、循環リストの場合は、Tangle の最初のインスタンス (このチェーン全体を開始したもの) に対して operator<<() への呼び出しが再度行われます。これが実行されると、operator<<() はすでにこのインスタンスを検査したことを認識し、saveGuts() を再度呼び出すことはせずに、前に書き込まれたリンクへの参照を作成します。これで連続的な再帰呼び出しが停止し、スタックが解放されます。
RWFile& operator>>(RWFile&, RWCollectable*&);
多重継承
第 14 章「RWCollectable クラスの設計」では、RWCollectable から継承して Bus クラスを構築しました。既存の Bus クラスがあれば、多重継承を使用して、Bus と RWCollectable の両方の機能を持つ新しいクラスを生成することができ、おそらく作業をいくらか軽減することができたでしょう。
class CollectableBus : public RWCollectable, public Bus { . . . };
また、この方法は、あいまいな基底クラスを避けるためにも望ましいと考えられます。次に例を示します。
class A { }; class B : public A { }; class C : public A { }; class D : public B, public C { }; void fun(A&); main () { D d; fun(d); // どちらの A か ? }
fun((B)d); // B の側の A の出現を意味する
2 番目の方法は A を仮想基底クラスにするというもので、上記の問題の解決にはなりますが、また別の問題をもたらします。キャストを派生クラスへのポインタに戻すことがほとんど不可能になるのです。なぜなら、継承階層の中を戻るパスが 2 つ以上できてしまうからです。より物理的な理由をいえば、コンパイラが仮想基底クラスを基底クラスへのポインタとして実装するので、ポインタを逆方向にたどれないということです。(ARM2 10.6c 参照)
オブジェクトの継承階層内で可能性のあるすべてのパスを徹底的に検索して、一致を検索することはできます (これは、NIH クラスの方法です)。しかし、この検索はすべてのキャストに対して実行しなければならないので、結果のアドレスを「格納」することによって処理速度を上げても、時間がかかります。また、処理量も膨大で必ず複雑になるので、受け入れられないものと判断しました。
したがって、最初の方法を選択しました。これは、同じ基底クラスからすべてを派生させるという方法を避け、継承ツリーを単純なままに保つことで、許容できるものになります。このため、機能の多い大きな一般基底クラスを使用するのではなく、それぞれの機能を独立したより小さい基底クラスに分けることにしました。
この考え方は、まず目的のオブジェクトを生成し、それから必要な機能 ("コレクタブルかどうか" など) を提供する基底クラスに追加することで、同じ型の基底クラスが複数になって呼び出しがあいまいになることを避けるというものです。
2 M.A.Ellis, Bjarne Stroustrup 共著、Addison-Wesley 出版、"The Annotated C++ Reference Manual" の略。日本語版は足立高徳、小山裕司訳、アジソンウェスレイ・トッパン、『注解 C++ リファレンス・マニュアル』