UDN
Search public documentation:

CodingStandardJP
English Translation
中国翻译
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

コーディング基準

Epic には、簡単なコーディング基準や慣例がいくつかあります。以下の例のほとんどは Unreal Script に関するものですが、この基準や慣例は C++ のコーディングにも適用されるべきです。この文書は、議論や作業のためではなく、Epic の現在のコーディング基準の状態を反映しており、ライセンス取得者に便宜を図るために提供されています。なお、エンジンには、レガシー・コードが数十万行ほどあって、新しいコードを追加したり、昔のコードのリファクタリングを行ったりしながら更新されているので、この基準があまり反映されていないことに注意してください。

コードの慣例は、いくつもの理由で、プログラマにとって重要です。:

  • ソフトウェアの生涯コストの 80% は、メンテナンス コストです。
  • 最初にコードを書いたプログラマだけがメンテナンスし続けるようなソフトウェアは、ほとんどありません。
  • 慣例化されたコードはソフトウェアの読みやすさを向上させ、エンジニアに、新しいコードをよりすばやく完全に理解することを可能にします。このプロジェクトのライフタイムの間に、弊社が新しいエンジニアやインターンを雇うことは確実であり、このプロジェクトで新たに施された変更は、おそらく以後のプロジェクトでも使われるはずです。
  • ソースコードを MOD コミュニティの開発者に公開する際にも、コードを理解しやすくすることは重要です。
  • これらの規則の多くは、実際にコンパイラ間の互換性を維持するためにも必要です。

Class (クラス) の構成: クラス名は名詞

  • クラス コメント ブロック
  • クラス宣言
  • 変数宣言
  • C++ のみ: static (静的) 変数、および、const (定数) 変数
  • Public (パブリック) 変数
  • Protected (プロテクテッド) 変数
  • Private (プライベート) 変数
  • cpptext
  • コンストラクタ、および、デストラクタ
  • BeginPlay( )、Destroyed( ) などを含む
  • 機能ごとにグループ化されたメソッドやステート(状態)
  • 機能ごとにグループ化されたステートレスな(いかなる状態にも関連付けられていない)ユーティリティ メソッド
  • 特定の状態への遷移・脱出に関連するステートレスなメソッドは、対応するステートの直前に置いてください。
  • 自動状態およびそれに関連付けられたメソッドがある場合には、他の状態より先に定義してください。
  • defaultproperties (デフォルトのプロパティー)

State (状態) の構成: 状態名は形容詞

  • 状態コメント ブロック (メソッド コメントと同様)
  • メソッド
  • Begin: コード

メソッドの構成: メソッド名は動詞

  • メソッド コメント ブロック
  • ローカル変数

変数: C++ 固有の型を使う

  • UBOOL: ブール値 (4 バイト)。BOOL ではコンパイルされません。
  • TCHAR: 文字 (TCHAR のサイズに依存するコードを書かないこと)
  • BYTE: 符号なしバイト (1バイト)
  • SBYTE: 符号付きバイト (1バイト)
  • WORD: 符号なしの 「short」 (2バイト)
  • SWORD: 符号付きの 「short」 (2バイト)
  • UINT: 符号なし整数 (4バイト)
  • INT: 符号付き整数 (4バイト)
  • QWORD: 符号なし「4倍長語」 (8バイト)
  • SQWORD: 符号付き「4倍長語」 (8バイト)
  • FLOAT: 単精度浮動小数点数 (4バイト)
  • DOUBLE: 倍精度浮動小数点数 (8バイト)
  • PTRINT: ポインタを保持できる整数 (PTRINT のサイズに依存するコードを書かないこと)

コメント

コメントはコミュニケーションであり、コードに_不可欠_なものです。以下は、コメントについて留意すべき事項です (Kernighan & Pike の 「The Practice of Programming」 (プログラミング作法)より):

  • 自己文書化するようなコードを書いてください。
わるい よい
t = s + l + b; TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;

  • 明らかなことは書く必要はありません。そのような場合には、コメントを省略するか、何か役に立つことを書いてください。
わるい よい
//iLeavesをインクリメント(段階的に増加)する //まだお茶の葉が残っていることがわかっている
Leaves++; Leaves++;

  • 読みにくいコードは、コメントで誤魔化さず、書き直してください。
わるい よい
//葉の数の合計は  
//大きい葉の数と小さい葉の数の足した数から  
//大きくかつ小さい葉の数を引いたもの  
t = s + l + b; TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;

  • コードを矛盾させないでください。
わるい よい
//iLeavesを絶対インクリメントしないでください。 //まだお茶の葉が残っていることがわかっています。
Leaves++; Leaves++;

弊社のドキュメンテーション(文書化)スタイルは、Javadoc に基づいています。 最終的には、説明文書は自動生成される予定です。

以下の例は、クラス、状態、メソッド、変数のコメントの書式を示しています。コメントはコードを補足するものであるということを忘れないでください。実装を文書化するのはコードであり、意図を文書化するのがコメントなのです。したがって、部分的にでも、コードの意図が変わったときには、コメントも更新するようにしてください。

class Tea extends WarmBeverage
   native, perobjectconfig, transient;
/**
 * このクラスは、たくさんの巧みな処理を行います。
 * このクラスは、他のいくつかのクラスと連携して働きます。
 */

/**
 *  中国のお茶の値段を保存する変数。
 */
var float Price;


/**
 * state Brewing
 * この state (状態) は、カップの中で Brewing (お茶を入れる際の蒸らしている) 状態を表すために作られました。
 * 入った時点: デフォルトの状態では、AddBoilingWater が呼ばれます。
 * 終わった時点: Pour (注ぐ) によって、もしくは、経過時間が長すぎた場合
 * (GettingBitter (苦くなる) に進む)
 */
state Brewing
{

/**
 * Steep は、指定されたお茶を蒸らす水の量および温度から
 * 味の差の値を計算します。
 *
 * @param    VolumeOfWater - お茶を蒸らすのに使った水の量
 *           ミリリットル単位の正の数(このメソッドでは正か負かはチェックされません!)
 *
 * @param    TemperatureOfWater - 絶対温度での水温。
 *           273 < temperatureOfWater < 380

 * @param    NewPotency - 蒸らし始めた後のお茶の濃さ。
 *           0.97~1.04の間である必要があります。
 *
 * @return   お茶の濃さの変化を、お茶味覚単位 (TTU)/秒で
 *           返します
 *
 */

function float Steep (float VolumeOfWater, float TemperatureOfWater,
                      out float NewPotency)
{
}
}

クラスコメントに含むべき事項

  • そのクラスが解決する問題の説明がひつようです。このクラスがなぜ作成されたか。

状態コメントに含むべき事項

  • その状態が解決する問題の説明。そのオブジェクトには、なぜそのような状態があるのか。上のクラス関連コードの順番からわかるように、状態に関連しているが、状態の中には含まれない関数は、状態コメントの直前に配置するべきであることに注意してください。
関数コメントは何を意味するのでしょう?
  • Brewing::Steep は、Steep メソッドが Brewing 状態の中にあることを意味しています。この Unreal Script コメントの中では、C++ のスコープ解決演算子が使われています。
  • 次は、この関数の目的です。ここでは、この関数が 解決する問題 を文書化しています。上で述べたとおり、 意図 を文書化するのがコメント、実装を文書化するのがコードです。
  • 「入力」のところには、このメソッドの入力パラメータのすべてをリストアップします。各パラメータのコメントには、単位、可能な値の範囲、「不可能な」値、および、ステータス/エラーコードの意味が含まれている必要があります。
  • 「入力/出力」のところ(この例では不要なので省かれています)には、関数内部で変更可能な値を関数に渡すパラメータを記します。入力パラメータでもあり、かつ、出力パラメータでもあるものを記します。
  • 「出力」のところには、出力のみの (呼出し元によって渡された値は無視される) パラメータのすべてをリストアップします。戻り値の意味や、エラー/障害の場合に何が起こるかも記載する必要があります。
  • return には、出力変数と同じように、戻り値として可能性のある値を記載します。

特殊なコメント。 以下のコメントでは、スラッシュとコメントの間にスペースがないことに注意してください。これにより、検索時に余計なコードにヒットすることが少なくなり、検索が楽になります。
//@debug リリースの前に削除されるデバッグ文す。
//@todo まだやるべき作業が残っています。説明に従ってください。

デバッグコードは、通常、エンジンから削除されるべきものであることに注意してください。デバッグコードを残す必要がある場合には、コンパイルはされるが、最適化時に削除されるように、#if 0 ではなく if(0) のブロックで囲む必要があります。そして、そのコードがデバッグ用であるという説明、プログラマの名前、および、デバッグコードの目的を記載しておいてください。

第 3 者によるコードの変更

エンジンの中で使われているライブラリ (wxWindows、FaceFX、Novodex など) のコードを変更する際には、変更部分に //@UE3 コメントでタグ付けして、変更理由を記載してください。これにより、その変更を新バージョンのライブラリに組み込んだり、ライセンス取得者が変更部分を発見したりするのが簡単になります。

命名規則

変数、関数、状態、および、クラス名は、曖昧でなく明快で、説明的な名前である必要があります。名前のわかりやすさは、名前のスコープが広いほど重要になります。過度な省略名は避けてください。

変数は1つずつ宣言するようにして、コメントとして変数の意味をつけられるようにしてください。 これは、Javadoc のスタイルの要求事項でもあります。変数の前のコメントは、1行でも複数行でもかまいません。また、間に空白行を挿入することで、変数をグループ化することもできます。

変数名では、ThisIsAGoodVariableName のような言葉ごとの大文字化方式を使ってください。 「thisIsABadName」 (別名: キャメルケース) や 「this_is_a_bad_name」 (アンダースコア) のような方式は使わないでください。

ブール変数の名前は、真偽を訊ねる疑問文にしてください。また、bool を返す関数の名前も、同じようにしてください。ブール変数には、必ず接頭辞として b を付けてください。

/**
 * お茶の重さ(Kg)
 */
var float TeaWeight;

/** お茶っ葉の数  */
var int TeaNumber;

/** TRUEは、お茶が臭いことを示す */
var bool bDoesTeaStink;

/**
 * 人間には読めないお茶の FName
 */
var name TeaName;

/** 人間に読めるお茶の名前 */
var String TeaName;

構造型の場合には、変数名の最後にクラス名を付加します。

/**
 * 使用するお茶のクラス
 */
var class<Tea> TeaClass;

/**
 * お茶を注ぐ音
 */
var Sound TeaSound;

/**
 * お茶の画像
 */
var Texture TeaTexture;

C++ コードの場合、デフォルトでは全クラス変数を private (プライベート) にしてください。変数を public (パブリック) にするのは、スクリプトとの相互作用が必要な場合(つまり、XxxClasses.h から生成された変数)だけにしてください。

プロシージャ(戻り値のない関数)の名前には、意味のはっきりした動詞と目的語 (オブジェクト) の並びを使ってください。ただし、メソッドの対象が、そのメソッドが所属するオブジェクト自体である場合は例外です。この場合には、内容から対象がわかるはずなので。「Handle」 や 「Process」 のような動詞は曖昧なので、これらで始まる名前は避けてください。

Tea SomeT;
SteepTea(SomeT);    // オブジェクトを処理するタイプのメソッド名
SomeT.Pour();       // Teaから呼び出されるメソッド。動詞だけで十分。

値を返す関数の場合には、戻り値も関数名に取り込んで、関数が返す値を明らかにする必要があります。これは、特にブール関数の場合に重要です。次の 2 つのメソッドの例を考えてみましょう。

function bool CheckTea(Tea t) {...} // TRUE が何を意味するかわからない。
function bool IsTeaFresh(Tea t) {...} // TRUE はお茶が新鮮であることを示すのが、名前を見れば明らか。

読みやすくするため、状態名やクラス名は、大文字で始まるようにし、名前の途中でも読みやすいように大文字を使ってください。クラス名は名詞にしてください。また、状態名は、対応する状態を表す(形容詞など)ようにしてください。状態は、クラスなしで使われることはないので、常に暗黙のオブジェクトが存在します。

渡された変数は必ず利用するようにしてください。使わない値がある場合には、その値をパラメータ・リストから外すか、パラメータの型だけ残し、パラメータをコメントアウトしてください。

void Update(FLOAT DeltaTime, UBOOL /*bForceUpdate*/);

Unreal Script: 参照渡しのパラメータには、out_ 接頭辞を使うようにしてください。

function PassRefs (out float out_wt, float ht, out int out_x)

Const

C++ では、const が使える場合には、必ず const を使うようにしてください。特に、関数パラメータやクラスメソッドでは必須です。const は、コンパイラに対する指示であると同時に、文書化でもあります。

Virtual (バーチャル、仮想)

派生クラスで仮想関数を宣言して、親クラスの仮想関数をオーバーライドする際には、*必ず* virtual キーワードを使うようにしてください。C++ の規格によれば、「virtual」 の動作も継承されるので、必ずしもそうする必要はないのですが、仮想関数宣言では必ず virtual キーワードを使ったほうが、コードははるかに明解になります。

実行ブロック

中括弧 { }

中括弧論争は醜いものです。Epic では、改行して新しい行に中括弧を置く方式を、長年にわたって使用してきました。ですから、どうかこの方式にしたがってください。

if - else

if-else 文中の実行ブロックは、すべて中括弧で囲んでください。これにより、編集ミスを防ぐことができます。というのも、中括弧が使われていないと、気づかないうちに if ブロックに行を追加してしまう可能性があるからです。追加した行は、if 式の制御対象にならないので、バグの原因になる可能性があります。もっとひどいときには、条件つきでコンパイルされた行によって、if/else 文がブレークさせられてしまうことがあります。ですから、必ず中括弧を使ってください。

if (bHaveUnrealLicense)
{
   InsertYourGameHere();
}
else
{
   CallMarkRein();
}

多分岐 if 文は、各 else if が最初の if と同じインデント位置に来るようにインデントしてください。この方が読む人から見て構造がわかりやすくなります。

if (TannicAcid < 10)
{
   log("Low Acid");
}
else if (TannicAcid < 100)
{
   log("Medium Acid");
}
else
{
   log("High Acid");
}

タブ

コードのインデントは、実行ブロックごとに行ってください。クラスはブロックに入っていないので、Unreal Script のクラスメンバ関数をインデントする必要はありません。コードのインデントには、スペースではなくタブを使ってください。そうすれば、各プログラマが自分の好きなタブ位置でコードを表示することができます。

switch 文

空の case (複数の case が同じコードを共有する場合) を除き、switch 文中の case の制御が次の case にそのままフォールスルーする場合には、そのことをコメントで明示的に示すようにしてください。つまり、case には、break 、もしくは、フォールスルーを示すコメントのどちらかがあるようにしてください。もちろん、break 以外のコード制御移行コマンド (return、continue など)でもかまいません。

また、default を必ず設けてください。そして、誰かが default の後に新しい case を追加したときのために、default の後にも break を書いておくようにしてください。

switch (condition)
{
   case 1:
      --code--;
      // フォールスルー
   case 2:
      --code--;
      break;
   case 3:
      --code--;
      return;
   case 4:
   case 5:
      --code--;
      break;
   default:
      break;
}

デフォルトプロパティは、継承された変数の次にクラス変数の順で並べるようにしてください。UE3 の場合、defaultproperties ブロックでは config 変数や localized 変数を定義できなくなったので注意してください。

defaultproperties
{
   // Objectから継承した変数
   bGraphicsHacks=TRUE

   // WarmBeverageから継承した変数
   bThirsty=FALSE

   // クラス変数
   bTeaDrinker=TRUE
}

論理式(ブーリアン式)

論理式の値としては、必ず TRUE や FALSE を使うようにしてください。true や false を使うと、コンパイラや定義でおかしな問題を引き起こす可能性があるのでやめてください。また、0 や 1 も、読者の誤解を招くので使わないでください。

local bool を宣言してコードを読みやすくすることは、ご遠慮なくやってください。下の 2 番目の例では、DoSomething() が呼び出される条件を変数名によってわかりやすくしています。

if ((Blah->BlahP->WindowExists->Etc && stuff) &&
    !(Player exist && Gamestarted && player still has pawn &&
    IsTuesday())))
{
   DoSomething();
}

次のように書き換えた方がよい。

local bool bLegalWindow;
local bool bPlayerDead;

bLegalWindow = (Blah->BlahP->WindowExists->Etc && stuff);
bPlayerDead = (Player exist && Gamestarted &&
               player still has pawn && IsTuesday());

if ( bLegalWindow && !bPlayerDead )
{
   DoSomething();
}

注意:式の途中で改行する際には、演算子の直後(つまり、演算子が前の行の末尾にくる)で行います。そして、以後の行は、対応する括弧の内側に揃うように配置してください。

また、コードが「単純すぎる」からといって、関数に書き出すことを躊躇しないでください。たとえば、上の bLegalWindow 変数が複数の関数で使われている場合には、(パラメータが簡単に利用できるとすれば)これ単独で関数にしてしまうのもよいでしょう。

一般的なスタイルの問題

  • 単一の関数に複数の return を書くことを避ける。 そのせいでコードが極端にわかりにくくならない限りは、return 文は一つだけにしてください。これにより、実行経路の追跡や、デバッグ用の修正が簡単になるので、コードのメンテナンスが楽になります。
  • debugf/warnf/appErrorf 関数に構造体を渡さない。 FNames や FStrings は、%sで印字できるように、* を使って渡してください。
  • 依存距離を最小化する。 コードが特定の変数値に依存する場合には、その変数が使われる直前で変数値を設定するようにしてください。実行ブロックの冒頭で変数を初期化してから、その値を使うまでの間に 100 行ものコードがあると、依存関係に気づかずにうっかり値を変更される危険が大きくなります。初期化した変数が次の行で使われていれば、変数をその値で初期化した理由も、変数が使われる場所もはっきりします。Berkeley の研究によれば、使用する行から7行より離れたところで宣言されている変数は、誤って使われる可能性が極めて高い (> 80%) ということがわかっています。
  • 関数の長さ。 関数は、60 行より長くしないでください。これは_恣意的な数です_が、印刷したときに関数全体が1ページにを収まる長さです。これより長い関数がある場合には、そのタスクをリファクタリング(わける)する方法を考えてみてください。呼び出しのオーバーヘッドを避けるには、短い関数にしてインライン関数を使うようにしてください。
  • コンパイラの警告を解決する。 コンパイラの警告メッセージは、何かがあるべき形になっていないことを意味しています。コンパイラが警告を出したところは、修正するようにしてください。もし、どうしても解決できない場合には、#pragma を使って警告を抑制してください。ただし、これは最後の手段です。
  • ファイルの最後には空白行を残す。 .cpp や .h ファイルを gcc でうまく動かすには、空白行が含まれている必要があります。
  • FLOAT から INT への暗黙の型変は避ける。 この演算は遅いし、どのコンパイラでもコンパイルできるとは限りません。INT に変換する際には、代わりに appTrunc() 関数を使うようにしてください。これにより、より高速なコードが生成されると同時に、コンパイラ間の互換性が保証されます。

/**
 * このクラスは、さまざまな波形動作を扱うクラスです。
 * 指定されたゲームパッドで指定された時刻に再生する波形データを管理します。
 * このクラスは、プレーヤコントローラによって、指定されたゲームパッドの波形を再生/停止/一時停止するために、呼び出されます。
 * また、UXboxViewport は、このクラスを呼び出して、現在の振動状態情報を取得して、それをゲームパッドに適用します。
 * この処理は、波形サンプルデータの中で定義された関数を評価することによって実現されます。<BR>

 *
  * 著作権:    Copyright (c) 2005<BR>
  * 会社:      Epic Games Inc.<BR>
 */
class WaveformManager extends Object within PlayerController
   native
   transient
   poolable(1,0);

/**
 * プレーヤが、ゲームパッドの振動を無効にしたかどうか (TCR C5-3)。
  * このコメントは長いので、実際に複数行が必要。
 */
var bool bAllowsForceFeedback;

/** 現在再生中の波形 */
var ForceFeedbackWaveform FFWaveform;

/**  波形がプレーヤ コントローラによって一時停止されているか */
var bool bIsPaused;

/** 現在再生中の波形サンプル */
var int CurrentSample;

/** 波形開始以後の経過時間 */
var float ElapsedTime;

/** 全波形に対するスケーリング(拡大縮小)の量(ユーザー設定) */
var float ScaleAllWaveformsBy;

/**
 * ゲームパッドで再生する波形の設定
 *
 * @param Waveform 再生する波形データ
 */
シミュレーションされた最終的 function PlayWaveform(ForceFeedbackWaveform Waveform)
{
   // 現在のサンプルや継続時間をゼロクリアし、一時停止状態の場合には、それを解除する。
   CurrentSample = 0;
   ElapsedTime = 0.0;
   bIsPaused = FALSE;
   // 波形が有効であることを確認する
   if (Waveform != None && Waveform.Samples.Length > 0 &&
      bAllowsForceFeedback == TRUE)
   {
      // 再生する波形を設定する
      FFWaveform = Waveform;
   }
   else
   {
      FFWaveform = None;
   }
}

/**
 * 波形をクリアすることにより、波形を停止する
 */
シミュレーションされた最終的 function StopWaveform()
{
   // 現在の波形を削除する
   FFWaveform = None;
}

/**
 * ゲームパッドでの波形の再生を一時停止/再開する
 *
 * @param bPause TRUE なら一時停止、FALSE なら再開
 */
シミュレーションされた最終的 function PauseWaveform(optional bool bPause)
{
   // ゲームパッドを一時停止状態に設定する
   bIsPaused = bPause;
}

defaultproperties
{
   bAllowsForceFeedback=TRUE
   ScaleAllWaveformsBy=1.0
}

C++ の関数

/**
 * 各サブカメラ効果に対してポスト レンダリングを実行して、結果を蓄積する。
 * これにより、放射結果がフルスクリーンパスに追加されることを防ぎ、
 * 放射に 2 パス分のブラーをただで追加することができます。
 *
 * @param Viewport   描画するビューポート
 * @param RI      描画に使われるレンダリング インタフェース
 */
void UUCScreenEmissiveWithBloom::PostRender(UViewport* Viewport,FRenderInterface* RI)
{
   guard(UUCScreenEmissiveWithBloom::PostRender);

   checkSlow( EmissiveCamEffect && BloomCamEffect );

   // 放射のポスト レンダリングの結果が
   // バックバッファに描画されないようにする
   EmissiveCamEffect->DisplayOutput = 0;
   EmissiveCamEffect->PostRender( Viewport, RI );

   // カメラの放射効果の結果を、カメラの
   // ブルームカメラ効果で使うように設定する
   BloomCamEffect->EmissiveResultTarget =
EmissiveCamEffect->GetEmissiveResultTarget();
   BloomCamEffect->PostRender( Viewport, RI );

   unguard;
}


/**
 * リンク 0 に結び付けられた整数変数をインクリメントして、
 * リンク 1 に結び付けられた整数変数と比較し、
 * その結果に応じてインパルスを発火する。
 */
void USeqCond_Increment::Activated()
{
   check(VariableLinks.Num() >= 2 && "Decrement requires at least 2 variables");

   // 整数変数を 2 つ確保する
   INT Value1 = 0;
   INT Value2 = 0;
   TArray<INT*> IntValues1;
   GetIntVars( IntValues1, TEXT("Counter") );
   TArray<INT*> IntValues2;
   GetIntVars( IntValues2, TEXT("Comparison") );
   // カウンタ変数の全てをインクリメントする
   for (INT VarIdx = 0; VarIdx < IntValues1.Num(); VarIdx++)
   {
      *(IntValues1(VarIdx)) += IncrementAmount;
      Value1 += *(IntValues1(VarIdx));
   }
   // リンクされた変数をすべて加算して、実際の値を得る
   for (INT VarIdx = 0; VarIdx < IntValues2.Num(); VarIdx++)
   {
      Value2 += *(IntValues2(VarIdx));
   }
   // 値を比較して出力インパルスを設定する
   OutputLinks(Value1 <= Value2 - 0 : 1).bHasImpulse = 1;
}

C++ のクラス

/**
 * 全テンプレート配列の基底クラス。動的配列の割り当てを処理する。
 * 非効率な並列成長パターンにより、不必要なデータコピーが生じるのを防ぐために、
 * どの程度を「スラック」メモリを確保するべきかを決めるアルゴリズムを実装している。
 */
class FArray
{
public:
   /**
    * 配列ブロックへのアクセスを可能にする
    *
    * @return 配列を構成するメモリブロックへのポインタ
    */
   void* GetData()
   {
      return Data;
   }

   /**
    * 指定されたインデックスが配列境界の範囲内であるかどうか確認する
    *
    * @return インデックスが有効なら TRUE、無効なら FALSE

   UBOOL IsValidIndex( INT i ) const
   {
      return i>=0 && i<ArrayNum;
   }

   // ...

protected:
   /**
    * 配列を保持するメモリブロックへのポインタ
    */
   void*    Data;
   /**
    * 配列の現在の要素数
    */
   INT      ArrayNum;
   /**
    * 配列が realloc (再配置)することなく保持できる要素の数
    */
   INT      ArrayMax;
};