ホーム < ゲームつくろー! < クラス構築編 < 線形補間テンプレートクラスを作ろう!(iterator実装編)

区間補間テンプレートクラスを作ろう!(iterator実装編)


 前章でinterpolating補間テンプレートクラスのコンテナ部分は実装できました。この章はコンテナの情報にアクセスする専門クラスであるiteratorクラスを実装してみましょう。この章は・・・う〜ん、ちょっと大変ですが、テンプレートクラスを作ってみたい方はご覧になってみてください。




@ iteratorクラス宣言

 listなどでイテレータはどう使っているでしょうか?たぶん、beginメソッドで開始イテレータを取得してendメソッドで返される終端イテレータになるまでit++のような増加単項演算子で回しているはずです。値は(*it)と間接指定演算子で取得していると思います。つまり、イテレータを取得してから値を取得するまで、メソッドらしいメソッドは使っていないんです。interpolatingテンプレートから取得できるイテレータも同様の方法で値を取得しますので、公開して主要に扱うメソッドは多くありません。

 兎にも角にも、まずはiteratorクラスの宣言から見てみましょう。

interpolatingテンプレートクラス宣言
// イテレータ
class _iterator
{
   //////////////////////////
   // メンバ変数
   //////
   TRI **_ppFirst;          // イテレートの最初のポインタ位置
   interpolating *_pInterp; // アクセス元のinterpolatingオブジェクト
   VAL _Val;                // 現在の補間値
   X _cx;                   // 現在の距離
   X _Cum;                  // 累積距離
   X _Unit;                 // 単位
   size_t _Offset;          // オフセット値
   size_t _Max;             // 最大格納数
   size_t _CurPos;          // 現在の位置
   TRI *_pCur;              // 現在の点へのポインタ
   bool _Finished;          // 終了済みフラグ
  STRATEGY _Strategy;      // 補間計算方法

/// 以下メソッドが続く・・・//
};

 クラスの名前が「_iterator」となっていますが、これは名前衝突を避けるためです。このクラスは「interpolatingテンプレート内」で定義する入れ子クラスです。そうしなければ、メンバ変数にVALやTRIなど型引数名を置けません。

 イテレータクラスは思いの他沢山のメンバ変数を用います。

_ppFirstはイテレートする最初の配列位置を指すポインタです。イテレートはこのポインタ位置から始まります。
_pInterpはアクセス元のinterpolatingテンプレートオブジェクトです。
_Valは現在の補間値を格納しておく変数です。interpolatingテンプレートは補間値を計算して返す必要があるために、各イテレータがそれを保持する事になります。
_cxは現在の区間内での距離を保持します。こういうのは必要な値ですね。
_Cumは現在までの累積距離を保持します。こういうのも必要な値です。ちなみに、この値を直接使用することはありません。
_Unitは進める距離の単位です。1回イテレートする度にこの距離分だけ進みます。
_Offset_Max_CurPosはそれぞれ配列の要素を表します。_Offsetはinterpolatingが持つ配列の先頭に対して_ppFirstがどの位置に当たるかを表します。通常は0になる値です。_Max_ppFirstからイテレート可能な点の個数です。そして_CurPosが_Offsetから数えた現在の要素数(相対値)です。これらの変数の値を見ながら、イテレータが可能であるかどうか、必要な点の数が揃っているかなどを判断します。
_pCurは現在参照している点へのポインタです。_Offstや_CurPosなどで現在の位置を知る事は出来ますが、この変数は「終端に達しているかどうか」という判定を行う時に重要になってくるために必要です。
_Finishedはイテレータが最終点に達した時にtrueになるフラグです。言ってみればもうそれ以上イテレートできませんよと知らせるフラグです。
_Strategyは補間計算を担う補間ストラテジオブジェクトです。イテレータはこのオブジェクトに必要な情報を渡す役目を成します。


 一度に沢山のメンバ変数を説明してもイメージが沸かないと思います。以後、徐々に各変数の働きを見ていくことにします。




A メソッド実装

 iteratorクラスの動作は正直複雑です。少しずつ、慎重にメソッドを覗いてみることにしましょう。


○ コンストラクタ

 コンストラクタでは先に説明した変数を全て初期化します。あれだけありますのでちょっと大変ですが、コンストラクタを抜けた段階で、イテレートが可能な状態(もしくは出来ない状態)にします。

interpolating::iterator::iterator
// コンストラクタ
_iterator( TRI** ppTrio=NULL, interpolating *pInterp=NULL )
{
   // とりあえずエラーの出ない状態にする
   _pInterp = pInterp;
   _Offset = 0;
   _Max = 0;
   _Finished = true;      // 終端に達している状態
   _Unit = 1;             // 1単位に初期化
   _cx = 0;               // 初期距離
   _Cum = 0;              // 距離を初期化
   _ppFirst = ppTrio;     // 最初のポインタ位置
   _CurPos = 0;           // 補間出来ない状態
   _pCur = INTERPOLATING_ENDPTR; // 終端ポインタにする

   // 配列の有効性をチェック
   if(ppTrio!=NULL)
   {
      // オフセット数、最大更新数(=点の数)、現在の点の位置を初期化
      _Offset = ((size_t)ppTrio - (size_t)pInterp->GetFirstPtr())/sizeof(TRI*); // 先頭からのオフセット数
      _Max = pInterp->size() - _Offset;   // 最大更新数
      _Val = ppTrio[0]->_val;             // 初期値
      // 点の個数が2つ以上であればとりあえず補間可能
      if(_Max>=2)
      {
         _CurPos = 1;               // 最初の目標点は2点目にあるので1にする
         _pCur = _ppFirst[_CurPos]; // 2点目にする
         _Finished = false;         // 終端に達していない状態に変更
      }
   }
}


 最初に何も出来ない状態に変数を初期化します。細かいところは割愛します。ポイントは引数のppTrioという配列が有効である場合の処理です。_ppTrioが有効なポインタを指している場合、それが最初から数えて何番目の要素を指しているのかを調べ、_Offset変数に格納します。引数のTRIポインタ位置、そしてinterpolatingの持つ最初のTRIポインタの差を取って、TRIポインタ1つのサイズで割って、オフセット数を獲得しています。こういうのは知っておくと便利ですよ。ppTrioが指すポインタが有効なので、その初期値を_Valに格納します。さてその次、補間というのは点が最低2つ無いと成り立ちませんので、2つ以上ある場合は、現在の位置_CurPosを「1」にします。0じゃありません。現在のポインタ_pCurも_ppFirstが指す位置の次にします。ん〜、この辺りは必要な処理なんですが、説明するにつまらない所です(-_-;;



○ nextメソッド

 このメソッドがイテレータの主要メソッドです。このメソッドは現在の単位距離分だけ補間を進め、メンバ変数の全ての状態を更新するエントリメソッドです。

 次の状態になるためにどのような処理が必要か?まずは実装をご覧下さい:

interpolating::iterator::nextメソッド
// イテレータの更新
public: _iterator next( X &x)
{
   // 次の距離へ進め、標準化距離を定義
   X norm = Update( x );

   // 補間計算に必要な点の数を取得
   size_t pn = _Strategy.NeedPointNum();

   // 指定の点の確保
   TRI** _ppPoint = _Strategy.GetPtrAry(); // 補間点ポインタを格納する配列
   if(!SetPoint( pn, _ppPoint )){
      // 最終フラグが立っていたら
      // 終端イテレータを返す
      if( _Finished || *_ppFirst==INTERPOLATING_ENDPTR)
      {
         // すでに終了している状態
         SetTarminate(); // 終端イテレータに設定
         return *this;
      }
      else
      {
         // 最後の点に合わせる
         _Finished = true;
         _pCur =_ppFirst[_pInterp->size()-1];
         _Val = _pCur->_val;
         return *this;
      }
   }
   // 次の値に更新
   _Strategy.Calc( &_Val, norm);
   return *this;
}


 処理が複雑になりますので、独立できる部分はサブメソッドに分けています。引数のxが進める差分距離です。まずそれをUpdateメソッドに渡します。このメソッドでは実際に差分距離だけ進め、配列の要素番号の切り替え等を行って最終的に現在の区間の何割の位置に進んだかを返します。それがnormという変数に格納されてきます。normが1を超えることはありません。

 次の_Strategy.NeedPointNumメソッドは、補間計算に必要な点の数を返します。線形補間であれば2が返ります。次に_Strategy補間オブジェクトが持つ配列を直接取得して、そこに点を突っ込みます。配列を取得するのが_Strategy.GetPtrAryメソッド、そして配列に有効な点を格納するのがSetPointメソッドです。

 SetPointメソッドは現在の補間に必要な点を_Strategyの配列に格納します。この時、必要な点の数を満たさない場合がやがてやってきます。その場合メソッドはfalseを返します(そう実装します)。補間計算がもう有効でない時は2種類考えられます。1つは距離が飛び出してしまった直後です。この時は最後の点を与えます。もう1つは最後の点を越えてさらにイテレートした場合(すでに終了している状態)です。プログラム中盤の条件分はそれぞれの場合の適切な変数を与えています。

 SetPointメソッドで有効な点がうまく格納された場合、補間値が_Strategy.Calcメソッドで計算されます。すでに点の情報は入っていますので、引数には計算結果を格納する_Valと、区間割合であるnormを与えます。

 いずれの場合でも、nextメソッドは*thisを返します。

 以上のように大まかな仕事に分割すると、nextメソッドの役目もわかりやすいのではないでしょうか。後はUpdateメソッド、SePointメソッドを実装するだけです。




○ Update

 Updateメソッドの主要な役目は補間距離を進める事です。まずは実装をどうぞ:

interpolating::iterator::Updateメソッド
// 次の位置と標準化距離を算出
protected: X Update( X x )
{
   if( *_ppFirst == INTERPOLATING_ENDPTR )
      return 1;

   if( _pInterp->size()<=1 ) // 1点では補間できない
      return 1; // 最終点を指すようにする

   _cx += x; // 次の位置へ移動
   _Cum += x;
   // 次の距離の所属区間を確定
   while(1)
   {
      // 今の位置が存在するかチェック
      if( _CurPos >= _Max ) // 存在しない
         return 1;
      // 今の位置を通り過ぎていたら次の点へ
      if( _cx >= _ppFirst[_Offset+_CurPos]->_x )
      {
         _cx -= _ppFirst[_Offset+_CurPos]->_x; // 飛び出た差分距離を算出
         _CurPos++;
         continue;
      }
      break;
   }

   // ↑-- 現在の区間位置が確定 --↑ //
   _pCur = _ppFirst[_Offset+_CurPos];

   // 次の点に対する標準化距離を算出
   return _cx / _pCur->_x;
}


 少しごちゃごちゃしていますが、じっくり見れば難しい事はしていません。まず_ppFirstが指すポインタが無効な場合(=INTERPOLATING_ENDPTR)、何も出来ませんので1を返して終了です。有効なポインタですが点が1点しか無い場合も補間作業ができませんので終了させます。

 次からが本番作業です。引数に渡された差分距離だけ_cxに足し算して進めます。ついでに累積距離_Cumにも足しておきましょう。whileループ内に入りまして、現在の位置_CurPosがすでに配列を飛び越えていたらやはり終了になります。有効な配列を指している場合、_cxが区間を飛び越えたかどうかを判定します。現在の点の絶対要素位置は_Offset+_CurPosで、区間幅もそこに格納されていますので、それと大きさを比較します。

 通り過ぎていた場合(if文の中)、点の更新を行います。飛び出した長さを計算して_cxに格納し、現在の点の要素数_CurPosを1つ増やした後、ループの先頭に戻って更新した点の位置の存在をチェックします。後は同様の操作を繰り返して、通り過ぎない区間位置を確定させます。

 後は確定した位置のTRIポインタを_pCurに格納して、_cx/_pCur->xという比率計算によって区間割合を計算して返します。




○ SetPointメソッド

 Updateメソッドの後、補間計算に必要な点を特定して_Strategyオブジェクト内の配列に点を引き渡すメソッドです:

interpolating::iterator::SetPointメソッド
// 補間計算に必要な点を格納
protected: bool SetPoint( size_t num, TRI** ary )
{
   size_t LastPointNum = (_CurPos-1) + num;
   if( LastPointNum > _Max )
      return false; // 必要な点を確保出来ない

   // 前の点を基点に点をコピー
   memcpy( ary, _ppFirst+_CurPos-1, num * sizeof(TRI*) );
   return true;
}

 引数のnumが必要な点の数、aryが点を格納する配列へのポインタです。まず格納する点は「前の点から」である事に注意してください。これは最初に仕様として定めました。つまり、このメソッド内での基点は前の点です。「_CurPos-1」とあるのはそのためです。前の点の要素番号にnumを足すと、最後の点の要素番号(相対値)が計算できます。これが保持している点の数を上回った場合は、点を格納できないのでfalseを返します。格納可能な点がある場合は、第2引数のaryに点へのポインタをハードコピーします。

 以上で、主要なメソッドの動きは終了です。他に細かくて小さな働きをするメソッド(現在の値を取得するなど)がありますが、それは冗長な説明なので割愛します。

 イテレータはメソッドよりも演算子が重要になってきます。次は演算子の実装を見てみましょう。




B 演算子の実装

 イテレータの演算子は次の4つを定義します:

 ・ *間接指定演算子
 ・ ++増加単項演算子
 ・ ==比較演算子
 ・ !=比較演算子

==比較演算子以外はどれも大切な演算子になります。



○ *間接指定演算子

 この演算子はイテレータから補間値を取得するために必要です。(*it)などとした時に値を返すように実装します。

interpolating::iterator::operator *演算子
// 間接指定演算子
VAL& operator *()
{
   return _Val;
}

非常に簡単ですね。_Valには常に現在の値が格納されているはずなので、これだけでいいんです。



○ ++増加単項演算子

 現在の距離単位分だけ補間を進める演算子です。実はこれはもう実装済みです。この演算子の役目はnextメソッドと同じなためです:

interpolating::iterator::operator *演算子
// 増加単項演算子
void operator ++(int dumy)
{
   next(_Unit); // 次のイテレータに更新する
}

増加単項演算子は「it++」と「++it」という2種類があるのはご存知だと思います。この実装ではit++のみをサポートします。右からの動作をする単項演算子のオーバーロードをする場合、operator ++に引数を与えると決まっています。上のdumyというのはその為に設けているわけです。これが無い場合は、左からの動作を意味する事になります。



○ ==、!=比較演算子

 比較演算子はイテレータ同士の比較を行います。しかしこれは補間値が等しいかどうかを比較するものではありません。この演算子は「イテレータが特定のイテレータと等しいか否か」に使われます。特に終端イテレータとの比較が重要になります:

interpolating::iterator::operator ==演算子、!=演算子
// 比較演算子
// (==演算子はあまり意味を持たない)
bool operator ==( _iterator &src )
{
   if( src._pCur == _pCur ) // 指しているポインタが同じ
      return true;
   return false;
}


bool operator !=( _iterator &src )
{
   if( src._pCur != _pCur ) // 指しているポインタが違う
      return true;
   return false;
}


 実装部にも書かれていますが==比較演算子はあまり意味を持ちません。右辺にあるイテレータが持つ現在のポインタ_pCurと自分のもつそれが等しいかどうかを判定します。

 一方で!=比較演算子は重要です。forループで終端イテレータ(_pCur=INTERPOLATING_ENDPTR)との比較が必要になるためです。やっていることは==演算子の真逆です。




 ここまでの実装で、イテレータを通して「補間結果」を得る事ができる様になります。好きな単位で補間を歩む事ができますし、最後の点に到達する事も出来ます。残されたのは補間計算部分、イテレータの中にある_Strategy変数の実装です。イテレータとこの変数とのやり取りはもう殆ど固定していますから、後はその定義メソッドを実装するだけです。次の章でそれを見てみましょう。