Previous Next Contents Index


Copyright 1999 Rogue Wave Software
Copyright 1999 Sun Microsystems, Inc.

実装に関する注意事項

17


この章は、Tools.h++ にある程度慣れた上でお読みください。ここで説明する情報は、ライブラリを効率的に使用するためには必ずしもすべて読む必要はありません。しかし、特定のトピックに関する知識を強化して、各種の製品機能をどのように実装したかを確認し、新しい情報をチェックするためには必要です。


動的リンクライブラリ

Tools.h++ クラスライブラリは、Microsoft Windows 3.X の動的リンクライブラリ (DLL) としてリンクすることができます。DLL では、リンクは、ルーチンが実際に使用される実行時に起こります。この結果、ルーチンは必要な場合だけに獲得されるので、実行可能ファイルは小さくなります。その他にも、多くのアプリケーションで同じコードを共有できるので、アプリケーションごとに同じコードを重複して作成しなくてもかまわないという利点もあります。

Windows と C++ の技術の進歩は非常に速いので、必ず配布ディスク上のファイル TOOLDLL.DOC を見て、更新がないかをチェックしてください。

DLL の例

この節では、Tools.h++ の DLL を使用する Windows のサンプルアプリケーション DEMO.CPP について説明します。このプログラムのコードは、サブディレクトリ DLLDEMO に入っています。このプログラムは、よく知られているカテゴリである「Hello World」の例に分類されます。

図 17-1 デモプログラムのウィンドウ
しかし、このプログラムは、実行時にウィンドウに挿入できる Drawable オブジェクトのリンクリストを維持するという点で、少し変わっています。クラス RWSlistCollectables を使用して実装されたリストは、サブディレクトリ DLLDEMO に入っています。この節の説明では、読者が Windows 3.X でのプログラミングにある程度慣れているものと仮定しています。しかし、C++ との関係については知っている必要はありません。

DEMO.CPP コード

次に、Tools.h++ の DLL を使用する Windows のサンプルアプリケーションである DEMO.CPP のメインプログラムを示します。

/*
 * 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 のデストラクタが、そのウィンドウオブジェクトに割り当てられていたメモリーすべてを解放します。

DEMOWIND.H

このヘッダファイルは、クラス DemoWindow を宣言します。主な機能は、ウィンドウに挿入された項目のリストを維持する、myList と呼ばれる片方向リンクリストです。メンバー関数 DemoWindow::insert(RWDrawable*) を使用すると、新しい項目を挿入することができます。431 ページの「SHAPES.H からの抜粋」で定義されているクラス RWDrawable から継承するオブジェクトだけを挿入することができます。

メンバー関数 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

DEMOWIND.CPP ファイル

ここでは、クラス DemoWindow の公開関数である DemoWindow コンストラクタ、DemoWindow デストラクタ、メンバー関数 insert()registerClass()paint() の定義を検討します。次のリストに続いて、詳しいコメントを示します。

#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 の追加データ機能を使用したこの DemoWindowthis ポインタの格納です。この手続きは少し厄介ですが、非常に便利です。ここに入れられる値は、CREATESTRUCT 構造に示されます。これは、CreateWindow 関数によって生成される WM_CREATE メッセージを処理するときに検索することができます。
//5 このメンバー関数は、アプリケーションの最初のインスタンスのためだけに呼び出されます。
//6 大域関数 DemoWindow_Callback は、このウィンドウのコールバック手続きとして登録されます。
//7 Windows に対して、この Windows クラスの this ポインタ用スペースを取っておくように要求します。これは、Windows のハンドルを特定の DemoWindow に関連付けるために使用されます。
//8 デストラクタは clearAndDestroy() を呼び出して、myList に挿入されたすべての項目を削除します。
//9 メンバー関数 inseret(RWDrawable*) は、新しい項目をウィンドウに挿入します。RWDrawableRWCollectable から継承するので、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;                    // 長方形の境界
};

SHAPES.CPP からの抜粋

この DLL デモのために、RWCollectable から継承したすべてのメンバー関数の定義を提供する必要は実際にはありませんが、ここでは完全にするために定義を提供することにします。

#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 この関数は、RWRectangleRWFile に格納します。

他の形状 RWEllipseRWtext も同様の方法で実装されます。


書き込み時のコピー

クラス RWCString、RWWString、RWTValVirtualArray<T> は、コピーを最小化するため「コピー・オン・ライト (書き込み時のみコピーする)」という技法を使用します。この技法は、わかりやすい値にもとづく意味、考え方にしたがったもので、参照回数をカウントするポインタを実装し非常に高速です。

次のように、ある RWCString が次のコピーコンストラクタによって別の RWCString で初期化されると、2 つの文字列は、どちらか一方がそこに書き込みを行おうとするまで同じデータを共有することになります。

RWCString(const RWCString&);
どちらか一方が書き込みを行おうとした時点で、データのコピーが作られ、2 つの文字列は別々のデータを持ちます。このようにして書き込み時のみコピーを行うと、効率の良い文字列のコピー (特に読み取り専用コピー) が作成されます。

次の例では、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;}
};
この方法は、文字列のコピーを 1 つしか作らなくてよいので、一見魅力的です。つまり、setForeground() の呼び出しは効率的です。しかし、よく検討するとその結果の意味はでたらめになる可能性があります。foreground が指している文字列が変化したらどうなるのでしょうか。前景色 (foreground) が変化するのでしょうか。その場合、クラス Simple はその変化をどのようにして知るのでしょう。さらに保守の問題もあります。"color" 文字列を削除するには、まずそれを指しているものがまだ存在するかどうかを知る必要があります。

以下に、もっと簡単な方法を示します。

class Smart {
  RWCString  foreground;
  RWCString  background;
public:
  setForeground(const RWCString& c) {foreground=c;}
  setBackground(const RWCString& c) {background=c;}
};
foreground=c という代入は値の意味論を使用します。クラス Smart が使用する色は完全に明確なものです。書き込み時のコピーによって、処理は効率的にもなります。これは、文字列が変更されないかぎり、データのコピーが行われないためです。次の例では、"white" の単一コピーを、"white" が変更されるまで維持します。

Smart window;
RWCString color("white");

window.setForeground(color);	// white への 2 つの参照

color = "Blue";	// white への 1 つの参照。blue への 1 つの参照


RWStringID

多くの Rogue Wave クライアントは、RWCollectable クラスに対して、RWClassID で使用可能な範囲よりも広い範囲のクラス識別子を要求してきました。既存の多相持続ファイルとの下位互換性を維持するために、RWClassID の意味は変更していません。しかし、新しい種類のクラス識別子 RWStringID を追加しました。

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 を次のように使用すると、固定 RWStringID と、生成される RWClassID を持つクラスを指定することができます。

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");

識別子の存続期間

同じまたは異なるプログラムを実行するたびに多相持続性を提供するには、持続される各クラスの永久識別子が必要です。今までは、RWCollectable の永久識別子はその RWClassID でした。RWCollectable から派生する各クラスに対して、マクロ RWDEFINE_COLLECTABLE は、クラスとその RWClassID を永遠に関連付けるコードを生成しました。この識別方法は維持されていますが、Tools.h++ の現行バージョンでは、選択された RWStringID をクラスに永続的にリンクする、RWDEFINE_NAMED_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 を調べる
RWFactory には、次の新しい関数も含まれます。

void           addFunction(RWuserCreator, RWClassID, RWStringID);
RWCollectable* create(RWStringID) const;
RWuserCreator  getFunction(RWStringID) const;
void           removeFunction(RWStringID);
RWStringID     stringID(RWClassID) const;
RWClassID      classID(RWStringID) const;
Tools.h++ に提供されている RWCollectable と、固定 RWClassID によって定義されている RWCollectable を、前のバージョンの Tools.h++ とまったく同様に使用することができます。たとえば、次の共通プログラミングコードを使用することができます。

RWCollectable *ctp;                         // ポインタを割り当てる
if (ctp->isA() == SOME_CONST_CLASSID)      // 特定の処理を実行する
しかし、非永続 ClassID を暗黙指定するユーザー定義 RWStringID を持つ RWCollectable を使用する場合は、実行可能プログラムを実行するごとに、RWClassID の値が異なる可能性があることに注意する必要があります。これらのクラスには、上記のものに代わる次の 2 つの慣用法があります。

RWCollectable *ctp;
// ポインタを割り当てる
// 比較のために、既存の RWCollectable を使用する
// 比較は RWStringID の比較よりも速く処理される
if(ctp->isA() == someRWCollectablePtr->isA())
  // クラスインタフェースにコーディングできる
// ...
// 識別をハードコーディングする慣用法。文字列比較は
// int 比較よりも遅いので、少しだけ遅くなる。
// stringID() も辞書検索を使用する。
if (ctp->stringID() == "Some ID String")  {
  // クラスインタフェースにコーディングできる
}

RWStringID の実装の詳細

次のいくつかの節では、RWStringID の実装の詳細について説明します。仮想メソッドを追加しないで仮想機能を提供する方法に興味がある場合、または設計、効率などの特定の問題に関心がある場合は、これらの節を参照してください。

自動 RWClassID

自動 RWClassID は、0x9200 から 0xDAFF の範囲内にある未使用の RWClassID から体系的に作成されます。このような RWClassID は最大 18,687 個利用できます。したがって、よほど特別なプログラムでないかぎり、ID が不足することはありません。しかし、経験上、特別な状況がありえないとはいえないので、次のように警告しておきます。つまり、自動生成された RWClassID を持つ異なるクラスを、1 つのプログラムで、18,687 個を超えて作成し、使用することはできません。1

このことは、各クラスのオブジェクトの総数には何も影響しません。この数字は、各自のオペレーティングシステムとコンパイラの要件だけによって制限されます。当然、0x8000 以下の RWClassID のフルセット (さらに可能な 32767 個 の RWCollectable) にアクセスすることもできますが、これらは自動生成されません。これらは手作業で指定する必要があります。

静的メソッドによる仮想メソッドの実装

仮想メソッド isA() は「実行時の固有な」RWClassID を返すので、この仮想メソッドを使用して、各種のデータまたは関数ポインタが保存されるルックアップテーブルへのインデックスを提供することができます (C++ の組み込み vtables に似ています)。RWCollectables は、単一 RWFactory の存在にすでに依存しているので、この RWFactory インスタンスを使用して、ルックアップ情報を保持することを選択しました。

次の静的メソッドは、RWFactory インスタンス内の id を検索しようとします。

RWStringID  RWCollectable::stringID(RWClassID id);
このメソッドは、関連する RWStringID の検出に成功すると、それを返します。そうでない場合は、RWStringID("NoID") を返します。

次の静的メソッドも同様の方法で動作して、RWFactory インスタンスを調べて、sid に関連する RWClassID があるかどうかを調べます。

RWClassID  RWCollectable::classID(RWStringID sid)
メソッドは、id を検出するとそれを返します。検出しなかった場合は、RWClassID _ _RWUNKNOWN を返します。

多相持続性

RWCollectable の多相持続性は、新しいクラス RWStringID の追加によって影響を受けません。古い RWClassID が変更されていないかぎり、既存のファイルは、新しくコンパイルされリンクされた実行可能プログラムによって引き続き読み取ることができます。RWStringID を持つ新しいクラスは、古いクラスと自由に混在させることができます。永続 RWClassID を持たない collectable の記憶サイズは、自分のより大きなスペース要件を反映しますが、他の RWCollectable の記憶サイズは影響を受けません。

同じ RWStringID を持つ RWCollectable を含むコレクションは、同じ RWCollectable への複数参照が初めて現れたときだけに保存されるように、その RWStringID をストリームまたはファイルに 1 回だけ保存することに注意してください。

効率性

RWClassID は、時間とスペースの両方において RWStringID よりも効率的であるので、可能であれば引き続き使用することができます。RWStringID は次の場合に便利です。

RWStringID は、現行バージョンの Tools.h++ のもとでコンパイルされるすべての RWCollectable クラスに対して生成されます。この追加コード生成機能は、RWStringID を使用しないプログラムに対して、ほんのわずかな影響しか与えません。RWFactoryRWClassIDRWStringID からの検索を保持するためにより大きくなり、追加データを RWFactory に加えるために起動時間がほんの少し長くなるだけです。

識別子の衝突

RWStringID は識別子の衝突を緩和するのに役立ちますが、異なるクラスの RWStringID の衝突の可能性は残ります。衝突は次の場合に起こる可能性があります。

場合によっては、このような衝突は重要ではありません。自動的に生成される RWClassID は、相互に異なり、有効なユーザー定義 RWClassID すべてとも異なるように保証されています。仮想 isA() メソッド、stringID() メソッド、RWClassID にもとづくコンストラクタ検索はすべて引き続き正しく動作します。

しかし、衝突によって問題が生じる場合もあります。衝突するユーザー定義 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*&);
RWCollectables を扱う場合、これらの関数がどのように動作するかを理解しておくと役に立ちます。以下で簡単に説明します。

任意のコレクタブルオブジェクトに対して初めて左シフト (<<) 演算子の 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() を再度呼び出すことはせずに、前に書き込まれたリンクへの参照を作成します。これで連続的な再帰呼び出しが停止し、スタックが解放されます。

チェーンの復元は同じような方法で行われます。下記の関数への呼び出しはヒープから新しいオブジェクトを作成し、そのオブジェクトへのポインタか、前に読み取ったオブジェクトのアドレス、もしくは NULL ポインタのいずれかを返します。

RWFile& operator>>(RWFile&, RWCollectable*&);
最後の 2 つの場合では、再帰が停止してスタックが解放されます。


多重継承

第 14 章「RWCollectable クラスの設計」では、RWCollectable から継承して Bus クラスを構築しました。既存の Bus クラスがあれば、多重継承を使用して、Bus RWCollectable の両方の機能を持つ新しいクラスを生成することができ、おそらく作業をいくらか軽減することができたでしょう。

class CollectableBus : public RWCollectable, public Bus {
  .
  .
  .
};
これは、Rogue Wave のコレクタブルクラスのうちの多くのクラスでとられる方法です (たとえば RWCollectableString はクラス RWCollectable とクラス RWCString の両方を継承します)。基本的な概念は、まず目的のオブジェクトを生成し、次に RWCollectable クラスと組み合わせて全体をコレクタブルにするというものです。この方法では、クラス RWCollectable から継承したくないような状況でも、生成したオブジェクトを使用することができます。

また、この方法は、あいまいな基底クラスを避けるためにも望ましいと考えられます。次に例を示します。

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() への呼び出しを明確にするための方法は 2 つあります。つまり、これを次のように変更するか、または A を仮想基底クラスにします。

fun((B)d);                         // B の側の A の出現を意味する
最初の方法はエラーを起こしやすいので、ユーザーは正しいキャストを行うために継承ツリーの詳細を知らなければなりません。

2 番目の方法は A を仮想基底クラスにするというもので、上記の問題の解決にはなりますが、また別の問題をもたらします。キャストを派生クラスへのポインタに戻すことがほとんど不可能になるのです。なぜなら、継承階層の中を戻るパスが 2 つ以上できてしまうからです。より物理的な理由をいえば、コンパイラが仮想基底クラスを基底クラスへのポインタとして実装するので、ポインタを逆方向にたどれないということです。(ARM2 10.6c 参照)

オブジェクトの継承階層内で可能性のあるすべてのパスを徹底的に検索して、一致を検索することはできます (これは、NIH クラスの方法です)。しかし、この検索はすべてのキャストに対して実行しなければならないので、結果のアドレスを「格納」することによって処理速度を上げても、時間がかかります。また、処理量も膨大で必ず複雑になるので、受け入れられないものと判断しました。

したがって、最初の方法を選択しました。これは、同じ基底クラスからすべてを派生させるという方法を避け、継承ツリーを単純なままに保つことで、許容できるものになります。このため、機能の多い大きな一般基底クラスを使用するのではなく、それぞれの機能を独立したより小さい基底クラスに分けることにしました。

この考え方は、まず目的のオブジェクトを生成し、それから必要な機能 ("コレクタブルかどうか" など) を提供する基底クラスに追加することで、同じ型の基底クラスが複数になって呼び出しがあいまいになることを避けるというものです。



1 16 ビット DLL は、メモリにロードされている間自動 RWCClassID の蓄積も行います。

2 M.A.Ellis, Bjarne Stroustrup 共著、Addison-Wesley 出版、"The Annotated C++ Reference Manual" の略。日本語版は足立高徳、小山裕司訳、アジソンウェスレイ・トッパン、『注解 C++ リファレンス・マニュアル』


Previous Next Contents Index