ニコニコC++入門

入門サイトや市販の入門書では絶対に教えてくれない、C++の本当の使い方を教えます。

古くて新しい状態遷移

2005-07-26 00:18:26 | C++
 状態遷移はコンピューターサイエンスの総明記からある考え方で、とても重要なものです。オブジェクト指向全盛時代になって忘れ去られたと思う人も多いかもしれませんが、デザインパターンにはステートパターンという形で残っていますし、UMLにもステートチャート図が存在します。ほとんどのオブジェクトには状態というものがあります(言い過ぎか……)。忘れ去られたなんてとんでもありません。むしろ今になって見直されつつあるのです。

 状態遷移の全てをここで語るにはスペースが狭すぎます。それなのに構造化設計の書籍はオブジェクト指向のそれに駆逐されてしまいました。状態遷移を専門に扱った書籍も見つかりませんし、オブジェクト指向の書籍に書かれている内容は物足りません。ここは少し解説しなければならないでしょう。

 オブジェクトは状態を持っています。オブジェクトはメッセージを受信すると、イベントを発生し、結果として状態が変化したりしなかったりします。具体的にはメソッドが呼び出されると、状態に応じた処理をして、状態を変更して処理を終えます。

 イメージしにくいでしょうか? 一見は百聞にしかずと言いますから、とりあえず下の図表を見てください。

 

停止状態 再生状態 ポーズ状態
停止 無視 →停止状態 →停止状態
再生 再生を開始
→再生状態
再生を中断
→ポーズ状態
再生を再開
→再生状態

状態遷移表

ステートチャート図(UML)

 再生ボタンと停止ボタンしかない単純なCDプレイヤーです。ちょっとした機能として、再生中は再生ボタンがポーズボタンとして機能するようになっています。縦軸(黄色)はイベント、横軸(赤)は状態、青は処理を表しています。処理が終了すると矢印で示される状態に遷移します。例えば再生状態にあるときに再生イベントが発生すると、再生を中断してポーズ状態へ遷移することがわかります。再び再生が始まったりするようなことは無いわけです。

 このように単純な例なら簡単ですが、複雑になってくるとすぐに破綻してしまいます。そういう複雑な状態遷移は状態をネストして解決します(下図)。状態のネスト、状態の入れ子、サブ状態などと呼ばれています。

オープン クローズ
停止 再生 ポーズ
無視 開ける
→オープン
開ける
→オープン
開ける
→オープン
閉じる
→クローズ
無視 無視 無視
再生 閉じて再生
→クローズ
再生を開始
→再生(CDあり)
再生を中断
→ポーズ
再生を再開
→再生
停止 無視 無視 →停止 →停止

状態遷移表

ステートチャート図(UML)

 入れ子をどんどん深くしていけばもっと複雑な状態も表せるのですが、あまり深くなりすぎるようならオブジェクトが大きすぎるのかもしれません。

 図表を作成することで、各状態における処理を洗い出すことができることが解ったと思います。ところがいざこれをコーディングしようとすると、意外にもなかなか難しいのです。

 複数の状態を管理してイベント発生時に適切な関数を呼び出すように管理するために、ややこしいフレームワークを書いて、関数テーブルを作成しなければなりません。こうした手間を軽減するためにCASEツールを用いる方法もあります。状態遷移に対応したCASEツールならば、状態やイベントの追加も簡単ですし、どの状態のどのイベントがテストされていないといったような情報を集めることもできます。

 ところで状態遷移とオブジェクト指向の関連性はどうなっているのでしょうか? オブジェクト指向ではオブジェクトが状態を持ちます。古くからある状態遷移表を用いる設計では表がたいへん大きくなります。携帯電話機などでは、なんと全てのイベントを一つの表にまとめたりします。しかし、オブジェクトはシステムやモジュールといった単位と比べると極めて小さく、あまり大きな遷移表を作ることはないと思います。また記述方式も表だけでなく、状態のネストを視覚的に表現できるUMLのステートチャート(上の図を参照)を用いるのが流行しています。イベントはメッセージと結び付けられ、まるでオブジェクト指向のためにあつらえたかのようにピッタリとあてはまります。

 さて、あとはこれをコーディングするだけなのですが……。C++では状態をどのように記述するべきなのでしょうか? 古くからあるフレームワークを使うのも一つの手ですが、せっかくですからクラスを活用したいですよね。デザインパターンの本にあるステートパターンはどうでしょうか。これは各状態ごとに一つのクラスを設けるという、たいへん重装備なものです。はっきり言ってしまえば、普通はこんなものは使いません。オブジェクトの状態遷移をコーディングする方法はもっと単純なものです。

class CPlayer
{
public:
  enum eState {
   STOP, PLAY, PAUSE
  };
private:
  enum eState m_eState;
public:
  CPlayer() : m_eState(STOP) {}
  Play();
  Stop();
};

CPlayer::Play() {
 switch(m_eState) {
 case STOP:
  cd_seek(TOP);
  cd_play();
  m_eState = PLAY;
  break;
 case PLAY:
  cd_pause();
  m_eState = PAUSE;
  break;
 case PAUSE:
  cd_play();
  m_eState = PLAY;
  break;
 }
}

CPlayer::Stop() {
 if (m_eState != STOP)
  cd_stop();
 m_eState = STOP;
}

 上記のソースは、最初のCDプレイヤーをコーディングしたものです。拍子抜けするほどに簡単ですね。イベントは関数で、状態はm_eState変数で表されていることが解るでしょうか。

 気をつけて読んで欲しいのは、イベントの中で状態による処理を振り分けているところです。状態でswitchするのがカッコ悪いと思う人もいるかもしれません。しかしそれは本質的な問題ではありません。大切なのは状態遷移を忠実にコーディングできているかどうかであって、それができるのなら実装方法は何でもいいのです。

 よく状態遷移ベースのCASEツールに影響されて関数ポインタの2次元配列でマトリックスを作りたがる人がいます。しかしながらクラスというものには適度なサイズというものがあり、適度なサイズのクラスは上記のサンプル程度のコードでよいのです。マトリックスが必要なら、その前にクラスが巨大すぎるのではないかということを疑うべきです。

 またデザインパターンのステートパターンも不要です。オブジェクト指向がわかってくるとステートパターンの設計は誰でも直感的に思いつくでしょう。しかしステート間での情報共有は非常にややこしくならざるを得ず、全てのステートの組み合わせが1つのクラスであると考えれば、これもまたクラスが巨大すぎることを疑わせます。

 関数ポインタマトリックスとステートパターンの2大設計が、クラス設計時の状態遷移設計の理解と普及の妨げになっているのです。

class CPlayer
{
public:
  enum ePlayState {
   STOP, PLAY, PAUSE
  };
  enum eTrayState {
   OPEN, CLOSE
  }
private:
  enum eTrayState m_eTrayState;
  enum ePlayState m_ePlayState;
public:
  CPlayer() : m_eTrayState(CLOSE), m_ePlayState(STOP) {}
  Open();
  Close();
  Play();
  Stop();
};

CPlayer::Open() {
 if (m_eTrayState == CLOSE)
  cd_open();
}

CPlayer::Close() {
 if (m_eTrayState == OPEN) {
  cd_close();
  m_eTrayState = CLOSE;
  m_ePlayState = STOP;
 }
}

CPlayer::Play() {
 if (m_eTrayState == OPEN) {
  Close();
  Play();
 } else {
  switch(m_ePlayState) {
  case STOP:
   if (cd_exists()) {
    cd_seek(TOP);
    cd_play();
    m_ePlayState = PLAY;
   }
   break;
  case PLAY:
   cd_pause(TRUE);
   m_ePlayState = PAUSE;
   break;
  case PAUSE:
   cd_pause(FALSE);
   m_ePlayState = PLAY;
   break;
  }
 }
}

CPlayer::Stop() {
 if (m_eTrayState == CLOSE && m_ePlayState != STOP) {
  cd_stop();
  m_ePlayState = STOP;
 }
}

 状態がネストしている場合は、状態変数が別になります。大状態と小状態がしっかり別の変数になっていることに注意してください。その他の具体的なコーディングに目を奪われずに、状態とイベントが状態遷移表のごとく格子状に交差していることを理解してください。

 状態の遷移を組むとき、状態変数を作っただけで満足してはいけません。イベントと状態がごっちゃになるミスを犯さないようにしなければいけません。また状態のネストを見極めて、大状態と小状態を一つの状態変数で管理してしまうことがないようにしなければなりません。

 これらのミスはオブジェクトのコーディングを不必要に複雑化させ、たちの悪いバグを引き起こす原因になります。状態変数が受け持つ状態の範囲が適切かどうかは、状態遷移表やステートチャート図を書くことで判断できます。頭の中で整理したつもりでも、図表を書いてみると、簡単な間違いが残っていることに気づかされるものです。

 状態遷移を分析するということはオブジェクトが状態変数を持つというような単純な話ではないのです。下手な設計ですと、コードはかけても図は汚くなります。状態変数の数や役目が適切かどうか、イベントとの対応が適切かどうかを、図や表を用いて視覚的に判断するという話だったわけです。

 ところで今回の例には含まれていませんが、状態にはヒストリ(履歴)という概念があります。以前の状態を記憶しておくことがあるのです。例えば、メニューを開きなおしたときに、カーソルの位置が前回開いていた位置を指しているような場合を想像してください。こうしたことを実現するために複雑なコーディングをする必要はありません。大状態と小状態がきちんと別の変数で管理されていれば、状態変数を初期化するかどうかを切り替えるだけで、他には何の工夫もなく自然と実現できるのです。

 というわけで、ざっと簡単に状態遷移を解説してきましたがいかがでしょうか。状態遷移を整理すればオブジェクトの振る舞いを面白いほど簡単に実装できます。世の中には状態遷移の魅力に麻薬のように取り付かれた人たちがいるほどです。この世界はまだまだ奥が深いので、いろいろ検索してみると面白いですよ。

あなたの知らない例外

2005-07-25 23:58:57 | C++

 C言語の場合、関数の中でエラーが発生したことを呼び出し元へ伝える方法として一番ポピュラーなものは戻り値にエラーコードを入れておく方法です。しかしこの方法は戻り値とエラーコードを同時に返したい場合には使えません。エラーコードを正常な戻り値として処理してしまう事故も起こりがちです。グローバルなエラー変数を用意する方法は、エラーを見逃したり、エラーを処理する前に次のエラーが発生してしまう危険があります。エラー受け取り用変数へのポインタを引数で渡す方法は問題は少ないのですがスマートさに欠けます。

 例外はこれらの問題をスマートに解決します。しかしここを読んでいるような読者なら、それはもうわかっていると思います。これらのメリットがわかっていながら例外を使わないのは、そのメリットがたいして有用なものと感じられないからではないでしょうか。例外を使ってみたものの、コード量が減るわけでもなく、むしろコードは増えて、厄介な問題も出てきて、メリットよりデメリットのほうが大きく感じます。

 例外にあまり魅力を感じていない人のほとんどは、例外の使い方を間違って認識しています。そこで今回は例外の本当の使い方をお教えします。このページを読み終わったら、きっと例外を使いたくてたまらなくなると思いますよ。

char *foo();
void normal_proc(char *);
void error_proc(int n);
bar()
{
 char *p = foo();
 int n = reinterpret_cast<int>(p);
 if (n == ERROR_NO)
  error_proc(n);
 normal_proc(p);
}

 一般的な throw の使い方では、関数の実行中に問題が発生したときに、return のかわりに throw を使って関数を終了させます(別に関数の外に投げなくてもいいのですが、話がややこしくなるので省略します)。

bar()
{
 try
 {
  normal_proc(foo());
 } catch (int n)
 {
  error_proc(n);
 }
}

 return では戻り値の型とエラーコードの型が同一でなければなりませんでしたが、throw ではエラーの内容にあわせて好きな型を選べます。

 上記の例は極端な例かもしれませんが、処理系によっては sizeof(char *) == sizeof(int) とは限りませんから、これをしなくて済むのはたいへんよいことです。

 しかしここで喜んではいけません。例外を上手く活用するためには throw に与える型が鍵になります。例外を生かすも殺すも型の選び方一つなのです。もうお気づきでしょう。上記の例は、ダメな例です。

 さて、話を進める前に例外の動きを少し追ってみましょう。

class EReadError : public EFileError
{
};

func()
{
 if (error)
  throw EReadError(filename);
}

bar()
{
 try
 {
  func();
 } catch (EFileError &e)
 {
  cout << e.FileName << "へのアクセス中にエラーが発生しました。" << endl;
 }
}

 throw された例外は catch されるまで関数をさかのぼります。catch しなければいつまでも関数をさかのぼり、最後にはプログラムが終了してしまいますので、どこかで catch してあげないといけません。

 catch 文があればなんでもいいわけではなく、throw された例外と同じ型の catch 文でなければなりません。型が合わない catch は無視されます。

 ただし継承を使ったクラスが throw されている場合は、その継承もとの親クラス(スーパークラス)でもあるとして扱われます。

 つまり左の例のように、EReadError クラスは EFileError クラスでもあるので、EFileError として catch できるのです。

bar1(somefile &s)
{
 try()
 {
  somefile.foo(s);
 } catch(EFileReadError &e)
 {
 } catch(EFileWriteError &e)
 {
 } catch(EFileError &e)
 {
 }
}

bar2(somefile &s)
{
  try()
  {
  somefile.foo(s);
  } catch(EFileError &e)
  {
  }

 これを利用して、エラーの種類はクラスの型で表現します。エラーコードではなくクラスを利用し、継承を使うことで、エラーのツリー体系を作り出すことができます。

 エラークラスの継承がツリー体系になっていると、エラーのリカバリー処理は柔軟になります。細かく調べたければ右の bar1 のようにクラスごとに処理を書けばよく、あまり細かいことを知る必要が無いときは bar2 のように書くことができます。

 また somefile クラスがさらに継承され、ポリモーフィズムを活用していて、foo が EFileTimeOutError を返すように書き換えられていたとしても、bar1 や bar2 を変更しなくていいのです。

 これを突き詰めていくと、throw されるエラーは必ず1つのクラスを継承することになります。stl には exception というクラスがありますし、C++Builder の VCL には Exception というクラスがあって、それぞれのクラスをルートとした例外クラスのツリーを形成しています。

 各自で独自の例外クラスを作るよりは、こうして用意されている既存の例外クラスを継承したほうがいいでしょう。とにかく組み込み型や、例外とは関係ないクラスを throw してはいけないと思ってまず間違いありません。

CFoo::CreateKey(int key)
{
 for (int i = 0; i < m_max; i++)
 {
  SetKey(new CKey(key, i));
 }
}

CFoo::Function(int n)
{
 CreateKey(800 * n);
 CreateKey(25 + n);
 CreateKey(6210 / n);
 CreateKey(7 - n);
}

int main(int, char**)
{
  try
  {
  MainLoop();//この中のどこかで CFoo::Function を呼び出しています。
 }
 } catch (exception &e)
 {
  cout << "なんかエラーが出ました" << endl;
  cout << e.what() << endl;
 }
}

 例外が継承されていると、catch を書かなくても済むことが増えます。

 上記のプログラムは、メモリが確保できなかったらエラーメッセージを出して終了する仕様です。

 各関数の呼び出し終了時でいちいちメモリを確保できたかどうか確認するのはたいへんです。CFoo::Function を、例外を使わずに1つ1つエラーチェックするようなコードに書き換えることを想像してみてください。これがもしもっと複雑な関数だったらどうでしょうか?

 例外を使えばエラー処理を減らすことができます。コード量が減ればバグは減ります。

 もちろん、メモリが確保できなかった理由を詳細に調べてその場でリカバリーするような処理を書けなくなったわけではなく、そういうときは必要に応じて catch を増やせばいいのです。

 つまり例外として投げるオブジェクトは何でも良いわけでなく、一つの exception クラスから体系立てて継承しなければなりません。しかも体系の立て方でリカバリしやすいかどうかも変わってきます。

 どうでしょうか。なるほどと感心された人もいるでしょうし、なにか狐につままれたような騙された気になる人もいるでしょう。

 「ちょっとマシな石ころ」にしか見えなかった例外という奇妙な仕組みは、正しい使い方を覚えれば光り輝く宝石だったことに気づくはずです。何度も練習して、使い方のこつを覚えるようにしてください。

 

 さて、これでおひらきにしてもいいのですが、もう1つ、例外で避けて通れない問題を解決しておきましょう。メモリリークの問題です。

 例外とメモリリークは付き物であるかのように言われています。ところがこれは簡単に解決できます。

try
{
 char *a = new char[size];
 char *b = new char[size];
 func(a, b);
 delete a;
 delete b;
} catch(...)
{
}

 とりあえず上記の例を見てください。func の中で例外が発生すると delete が実行されません。

char *a = NULL;
char *b = NULL;
try
{
 a = new char[size];
 b = new char[size];
 func(a, b);
}
catch(...)
{
 throw;
}
delete a;
delete b; 

 その対策として上記のようにしたりします。ちなみに NULL を delete しても何も起きませんので大丈夫です。これでだいぶましになりますが、catch の中でさらに throw するとやっぱりリークしてしまいます。

class foo
{
private:
 A *m_pA;
 B &m_B;
 C *m_pC;
public:
 foo(X &x);
 ~foo();
};

foo::foo(X &x)
: m_pA(new A(x)), m_B(x.GetB(new A(x)))
{
 try
 {
  m_pC = new C(x);
 } catch(...)
 {
 }
}

foo::~foo()
{
 delete m_pA;
 delete m_pB;
}

 さらに頭を悩ませるのが、コンストラクタの中で発生する例外です。

 m_pC の初期化は例外を catch していますが、もしここで catch しなかったらどうなると思いますか?

 コンストラクタが完全に終了していない場合は、なんとデストラクタが呼び出されないのです。なるほど、コンストラクタの処理が最後まで終わっていないのにデストラクタが呼ばれるとまずいこともあるでしょう。

 それだけではありません。A のコンストラクタは catch すらできません。これはコンストラクタの {} の中に移動すれば済むとしても、m_B は参照なのでこの手が使えません。この中でも例外が発生することがありますから困ります。

 こんな難問を本当に解決する方法があるのでしょうか? しかも簡単に?

try
{
 auto_ptr<char> a(new char[size]);
 auto_ptr<char> b(new char[size]);
 func(a.get(), b.get());
} catch(...)
{
}

 ここで秘密兵器をご紹介します。auto_ptr という STL 標準のテンプレートクラスです。このクラスはスコープから抜けたら自動的に解放されるポインタとして使うことができます。

 さっき出てきたメモリリークする関数の例を、auto_ptr を使って書き換えたのが右上のリストです。スコープの外に出たら自動的に開放されますので delete が不要です。catch の途中で throw が投げられても開放されます。

class foo
{
private:
 auto_ptr<A> m_pA;
 B &m_B;
 auto_ptr<C> m_pC;
public:
 foo(bar &x);
 ~foo();
};

 またメモリリークするクラスの例では、メンバが auto_ptr になっています(上記)。こうするとコンストラクタが完了しなくてもメンバは自動的に開放されます。しかも確保してないメモリを間違って開放して困ることもありません。

 よい C++ のコードには delete がほとんどありません。deleteを無くすために打つ手には以下のようなものがあります。上から優先的に適用を検討してください。これらの方法に共通しているのは、デストラクタがスコープから出たときに必ず呼ばれるというC++の特性を利用しているということです。

  1. クラスの中にポインタを隠蔽する(クラスの外ではdeleteが不要)
  2. listやvectorなどのコンテナを利用する(配列をnew/deleteしない)
  3. auto_ptrを利用する(一時的なポインタを作成するときに非常に狭いスコープの中で使う)

 というわけで例外は理解できましたでしょうか。このページを読み終えたなら、エラー処理を書くのが凄く楽になっているはずです。


STLとイテレーター

2005-07-24 23:24:54 | C++

 STLというのはC++標準のテンプレートライブラリです。STLを知らないというのは、C言語で printf や malloc を知らないということと同じです。なんとしても覚えてください。

 さて脅しはここまでにしておきましょう。STLは膨大な規模のライブラリで、プログラミング言語の標準としては他に例を見ないほどのものです。また分量だけでなく、その構造も難解で、これからC++を習得しようとする初学者の壁となっていきなり立ちはだかります。STLの学習にくじけている人は少なくないはずです。

 しかし大規模で難解なSTLですが、無節操に多機能化しているわけではなく、むしろひとつの方針の上に体系化されていますので、その方針さえ理解できれば実はそれほど難しくありません。本を読んだりしていまいち納得できないのは、その方針がわかってないからでしょう。そこで今回はSTLが基盤としているデザインパターン、イテレーターについて説明します。

 STLには、コンテナ、アルゴリズム、ストリームに加え、いくつかの有用なテンプレートが収められています。コンテナとは可変長配列やリスト、キューなど、データを収めるための構造です。アルゴリズムとは、検索やソートなど、コンテナに対する操作一般です。ストリームはファイルや画面に対する操作です。この中で特に重要なのはコンテナとアルゴリズムです。

 例えば CData オブジェクトをリストで保存するなら list コンテナを使用します。その中から特定の CData を検索したければ find アルゴリズムで検索するだけです。双方向リストやキューや可変長配列といったコンテナや、ソートやサーチといったアルゴリズムをコーディングする必要は、C++を使っている限り一生ないのです。

 STLで用意されているコンテナやアルゴリズムはすべてのオブジェクトを扱うことができます。こうした汎用性はイテレーター(反復子)によって実現されています。

 イテレーターを簡単に言えば、仮想的なポインタです。ポインタについて基礎的なことがわかっていれば、イテレーターを理解するのは簡単です。まずはポインタによる検索ついて、復習がてらに見て下さい。

void foo()
{
    const int MAX = 256;
    CData d[MAX];
    CData *f = DataFind(&d[0], &d[MAX], CData(80));
    if (f == d[MAX]) throw Exception("Not Found");
}

CData *DataFind(CData *start, CData *end, CData &match)
{
  for (CData *i = start; i != end; i++)
    if (*i == match)
      break;
  return i;
}

 DataFind 関数は CData の配列から match と同じオブジェクトを見つけ出して返す関数です。

 この関数にはいくつか奇妙なところがありますが、気づかれましたか? 従来のCによるプログラミングでこうした関数を作る場合、データが見つからなければ NULL を返すのが一般的だったと思います。しかしC++ではこのように、配列の最後尾から1つはみ出したところを指していたら失敗となります。これはイテレーターパターンと関係があります。

 とにかく i というポインタが配列の先頭から最後まで1つづつ進んでいることに注目してください。またテンプレートを使用すれば、CData だけでなく、どのようなクラスにも適用できることに気づいてください。DataFind をテンプレートにすれば、以下のようになるでしょう。

template <class T> DataFind(T *start, T *end, const T &match)
{
  for (T *i = start; i != end; i++)
    if (*i == match)
      break;
  return i;
}

 しかしこの関数は配列にしか使えないという欠点があります。配列にしか使えない理由は簡単で、ポインタを使っているからです。この欠点を解決するために、ポインタではなくイテレーターを使います。

class iterator : public __it
{
protected:
  __link_type node;
public:
  bool operator== (const iterator& x) const { return node == x.node; }
  reference operator* () const { return (*node).data; }
  pointer operator-> () const { return &(node->data); }
  iterator& operator++ () { node = (__link_type)((*node).next); return *this; }
  iterator& operator-- () { node = (__link_type)((*node).prev); return *this; }
};

 上記は本物の list コンテナのイテレーターからの抜粋です。イテレーターはクラスオブジェクトですが、++ や -> といった演算子が利用されたときに、ポインタであるかのように振舞います。あるイテレーターがなんらかのオブジェクトへのポインタであるフリをしているとき、++ すると、次のポインタへ進んだフリをします。配列のイテレーターなら本当に++するだけですし、リストのイテレーターなら次のノードを参照するようにします。

 要するにイテレーターとは、どんな複雑なメモリ管理をしているコンテナも、単純な配列であるかのようにテンプレートをダマす、仮想的なポインタなのです。

 イテレーターはデザインパターンのひとつです。STLではイテレーターを積極的に活用することでコンテナとアルゴリズムの相互運用性を高めています。アルゴリズムを自作した場合でも、イテレーターパターンに従っていれば、どの標準コンテナに対しても使えるようになります。逆にコンテナにイテレーターを用意しておけば標準のアルゴリズムを使用することができます。そして、まったくお互いの存在を知ることなく作られたコンテナとアルゴリズムが、相互に利用可能になるわけです。

 STL を使用したり拡張したりするには、STL が決めたイテレーターの振る舞いに従う必要があります。例えば find(foo.start(), foo.end()) == foo.end() ならば find が失敗であることを保証しないといけません。STL はコーディング規約という側面も持っているのです。

 コンテナやアルゴリズムによって制限があることも知っておいて下さい。例えば binary_search はソート済みのコンテナに対してしか適用できませんし、sort はランダムアクセスできない list には適用できません(もちろんエラーが出ます)。

 こうした細かい制限は、各コンテナの性質で仕方がなかったということもありますが、それよりもC++の設計思想に深い理由があります。研究段階からいつまでも脱却できない他のプログラミング言語と違って、C++は現場から厳しい要求を受けています。C++はあらゆる面でパフォーマンスに対して丹念に配慮してあり、STL もまた例外ではないのです。

 イテレーターさえ理解できてしまえば、あとはヘルプや書籍を読むだけです。たくさんのコンテナやアルゴリズムがありますが、分量が多いだけで考え方はひとつしかありません。なんとかSTLを覚えて、楽チンプログラミングを堪能してください。もう双方向リストのバグで悩む必要が無いなんて、すばらしいと思いませんか?


テンプレートの正体

2005-07-24 23:14:54 | C++

 オブジェクト指向ではポリモーフィズムを活用することで再利用性を高めているということをすでにお話しましたが、C++はオブジェクト指向だけの言語ではありません。オブジェクト指向しかできないほかの言語とは異なり、便利なものは何でも取り入れようという非常に貪欲な設計方針で作られています。こうしたいわば節操の無いやりかたに対して、たびたび他のコミュニティから非難されてきました。

 しかし90年代初頭にSTLの原型が現れて、テンプレートへの評価は逆転したと言えるでしょう。JavaやC#には当初はテンプレートがありませんでしたが、genericsという形で移植されてしまいました。それほどこのC++のSTLは衝撃的なものだったのです。

 テンプレートのようなプログラミングスタイルはジェネリックプログラミング(総称)と呼ばれ、古くはマクロという形で実現されていたものです。マクロによるプログラミングは複雑に入り組み、読みづらくなり、安全性に疑問が残ります。簡単に言ってしまえば、テンプレートは型安全(タイプセーフ)を考慮したマクロです。そして、全く無関係であると思われていた2つの概念、継承と総称がテンプレートによって1つにまとまりました。

 今回はテンプレートのメリットや活用方法についての説明ではなく、テンプレートの仕組みについてのお話です。

#define max(a, b) (((a) > (b)) ? (a) : (b))

 この伝統的なマクロにはいくつかの問題があります。max(a++, b) は aが2回インクリメントされてしまいますし、max(foo(), bar()) もまた然りです。またこうしたマクロを自作する際には、カッコの付け忘れに注意しなければなりません。これらの問題はテンプレートを使用することで解決できます(下)。

template const T& max (const T& a, const T& b) { return a > b ? a : b; }

 どうでしょうか。何を意味しているのかわかりますか?

 テンプレートは一般に理解しにくいと言われています。一部の人は「またC++は複雑な機能を入れた」と言います。しかしテンプレートは実は驚くほど単純な仕組みによって実装されているのです。そしてそれがわかれば、どんな複雑に入り組んだテンプレートでも(簡単にとは言いませんが)理解できるはずです。

 では実際にテンプレートがどのように実装されているのか見てみましょう。

test1.cpp

CData &foo()
{
  CData left, right;
  return max<CData>(left, right);
}

test2.cpp

CMode &bar()
{
  CMode left, right;
  return max<CMode>(left, right);
}

 上記の2つのソースは、どちらもテンプレート関数 max を使用しています。

 max 関数が使用しているクラスTはまだ型が定まっていません。test1.cpp では CData になりますし、test2.cpp では CMode になってしまいます。それではいつの時点で、型が定まるのでしょうか。

test1.cpp 

CData &foo()
{
  CData left, right;
  return max(left, right);
}

const CData& max(const CData& a, const CData& b)
{
  return a > b ? a : b;
}

test2.cpp 

CMode &bar()
{
  CMode left, right;
  return max(left, right);
}

const CMode& max (const CMode& a, const CMode& b)
{
  return a > b ? a : b;
}

 上記のソースはテンプレートがコンパイラによって展開された直後のものです。赤い行は、コンパイラがテンプレートを元にして作成した関数(のイメージ)です。T の部分が置き換わっていることがわかると思います。

 このようにコンパイラは、テンプレートを利用している部分があると、そのテンプレートを元にして実際の関数を作り出してしまうのです。

 同じ関数があちこちにできてしまうこともあり、コード効率は非常に悪くなります。テンプレートを使用するとコードサイズが激増するので要注意です。このために組込み系ではテンプレートをほとんど使用できません(テンプレートの実体部分を一まとめにする機能を備えたコンパイラも存在します)。

 実際にはインライン化される場合がほとんどでしょう。

template <int N> int sum()
{
  int s = 0;
  for (int i = 0; i < N; i++)
    s += i;
  return s;
}

void foo()
{
  sum<16>() + sum<17>();
}

int sum<16>()
{
  int s = 0;
  for (int i = 0; i < 16; i++)
    s += i;
  return s;
}

int sum<17>()
{
  int s = 0;
  for (int i = 0; i < 17; i++)
    s += i;
  return s;
}

 テンプレートの<>の部分を、テンプレート引数と呼びます。テンプレートの正体は、テンプレート引数を置き換えるだけの仕組みです。従って上記のようなコードも書けます。

 赤い行は例によって、コンパイラが template のコードを元に自動作成したコードのイメージです。ここでは N の部分が置き換わっています。テンプレート引数の部分が置換されただけの同じ関数が2つできています。

 このようにテンプレート引数には class 以外のものも指定できるのですが、コード効率が悪くなるため、よほどの理由が無い限りこうした使い方はされません。

 こうして仕組みがわかれば、奇妙に見えたテンプレートも理解できるのではないかと思います。

 今回の記事ではテンプレートの効能について説明しませんでしたが、テンプレートが使えるのはC++の大きな魅力です。JavaやC#で使えるようになってしまったためにもはや専売特許ではなくなってしまいましたが、それでもテンプレートを利用したプログラミングはC++の大きなアドバンテージだと言えるでしょう。テンプレートを活用しないならC++の魅力は半減ですよ。


キャストとプログラムの品質

2005-07-24 22:29:23 | C++

foo(int *arg)
{
  int work = arg;
}

 もしコンパイラが上記のプログラムに対してエラーを出さなかったとしたらどうでしょうか?

 C言語とC++言語は、Pascal ほどではありませんが、型のチェックが比較的厳しいプログラミング言語です。上記の例のような、うっかり間違って想定外の変数に代入しようとしてしまうような初歩的なバグを、コンパイルの時点でチェックすることができればバグの数も自然と減ります。

 コンパイルエラーを解決していくだけで多くのバグが取れるというのは実は画期的なことです。エラーのチェックが甘い、あるいは勝手に自動変換してしまう言語もありますが、それらの言語ではやはりデバッグに苦労します。ある程度とはいえ、コンパイラがバグを取ってくれるのですから、これを有効に利用しない手はありません。

foo(int *arg)
{
  int work = (int)arg;
}

 この厳しい型チェックはよく裏目に出て不便を強いられますので、C言語ではキャストを使用して強制的に型を変換できるようになっています(上)。しかしながら、C言語のキャストはすべての型チェックを無効にできる、たいへん強力かつ危険なもので、せっかくのエラーチェックを台無しにしてしまいます。そこでC++ではキャストが拡張されました。

foo(int arg)
{
  short work = static_cast<short>(arg);
}

  このC++で拡張されたキャストは、一見無意味で面倒なだけのようなものに見えるかもしれませんが、本当に無駄ならこのような拡張はなされないわけです。拡張されたキャストはバグを未然に防ぐ強力なツールとなります。上記の static_cast は、キャストをつけなくてもワーニングで済む種類のキャストに対してのみ使用できるキャストです。const 属性の変数に対する代入や、ポインタへの変換などは、代入が許されずにエラーとなります。

 C++には static_cast の他にも、const_cast、dynamic_cast、reinterpret_cast という4種類のキャストが用意されています。static_cast についてはすでにお話ししましたので、残りの4つを解説していきましょう。

 const_cast は const を無効化するためのキャストです(逆もできますけど)。C言語でのプログラミングにある程度精通されている人でも、const を使ったことが無いという人は多いかもしれません。const とは静的で変更不可能な定数のことです。この値はプログラムコード領域やROM領域などに置かれます。const を宣言した変数に対して変更を加えるとコンパイルエラーとなります。メモリが豊富なパソコンやワークステーションではエラーチェックされてもあまりうまみがありませんし、define や enum で済ませたほうが都合のよいことも多いでしょう。

 C++では const の重要度が飛躍的に増しています。C言語の const とC++の const は別のものだと言う人もいます。そこで、const_cast の話をする前に const の話をしておきます。

class CFoo
{
private:
  int m_nCount;
  int m_nMax;
protected:
  virtual bool CheckCount() const
    { return true; }
public:
  int SetCount(int count)
    { return m_nCount = count; }
  bool CountDown();
};

bool CFoo::CountDown()
{
  if (!CheckCount())
    return true;
  m_nCount++;
  return m_nCount >= m_nMax;
}

 上記のクラス CFoo は SetCount で指定した値まで、CountDown を使って1つづつカウントするクラスです。

 CBar は CFoo を継承しています。CFoo と同じ動作をしますが、CheckCount を変更しています。CFoo では CheckCount が false ならカウントを強制的に終了させることができるようになっており、CBar ではこれを利用して時間制限を設けています。

class CBar : public CFoo
{
protected:
  virtual bool  CheckCount() const;
private:
  bool IsTimeOver() const;
};

bool CBar::CheckCount() const
{
  SetCount(time); //error
  CountDown(); //error
  return IsTimeOver(); // no error
}

 ところが CBar::CheckCount はコンパイルできません。const が指定されたメソッドではメンバ変数の変更が禁止されるためです。CheckCount はメンバ変数の操作が禁止されており、またメンバ変数を操作するメソッドを呼び出すことも禁止されています。メンバ変数を変更するあらゆる手段が遮蔽されているのです。アクセスできるのは、メンバ変数を変更しないことが約束されているメソッド、すなわち const なメソッドだけです。

 const を利用すると、ヘッダファイルを見るだけでメソッドがメンバ変数を変更するかどうかが一目でわかるようになるため、エラーチェック以上にバグを減らす効果があります。

Sub(char *s);

Foo1(const char *p)
{
  Sub((char *)p); // no error
}

Foo2(const int p)
{
  Sub((char *)p); // no error
}

Bar1(const char *p)
{
  Sub(const_cast<char *>(p)); // no error
}

Bar2(const int p)
{
  Sub(const_cast<char *>(p)); // error

 エラーチェックに有効な const ですが、キャストするだけで const 属性を外すことができてしまいます。関数 Foo1 は引数 *p の const 属性を取りたいだけなのですが、キャストを使用すると Foo2 のようなバグでもエラーがチェックされなくなってしまいますので、お勧めできません。

 const_cast を使用すれば、根本的な型変換をさせずに、const や volatile 属性を付け外しできるようになります。Bar2 はコンパイルエラーとなりますので比較的安全です。またあとからソースを見直したとき、このキャストが const を外すことを目的としたキャストであると一目で判断できます。
 
 先ほどの CheckCount のような、メソッドに対して課した制限を無効にすることもできてしまいますので、メソッドに const を指定しても結局あまり意味がないように思われるでしょうか。

 const メソッドを設定するのは、クラスを作成する人のためではなく、クラスを使う人のためだと考えてください。実のところ、クラスを作成する立場からみると const なメソッドにはあまり恩恵がないのですが、クラスを利用する人にとっては大変役立ちます。クラスを作る場合、それを利用する立場で const を設定していくといいでしょう。

 const なメソッドの中でも、参照カウンタの増減など利用者からは見えない秘密の仕組みを操作したいことが度々あります。そういう場合、とりあえず const メソッドにしておいて見かけ上では値を操作しないように装います。利用者から見れば、メソッドが内部の値を操作していないように見えるわけです。

 このような秘密の仕組みを操作するためには、メソッドの中で const 属性を変更する必要がありますが、そこで古いタイプのキャストを使用すると、あらゆるエラーチェックが無効になってしまいます。const_cast は通常のキャストと違って const 属性を変更する以外の変換が禁止されますので、比較的安全です。const 属性はクラスを利用する人のためのものですが、const_cast はクラスを作る人のためのものだと言えるでしょう。

 次は dynamic_cast です。dynamic_cast は通称ダウンキャストと呼ばれ、C++のキャストの中でも特に複雑な事情があります。

 サブクラス(継承先のクラス)として作成したオブジェクトを、スーパークラス(継承元のクラス)としてアクセスするのは簡単です。スーパークラスのメンバ変数や関数に対しては、特別なことをせずともそのままアクセスすることができます。しかし逆の場合はそう簡単にはいきません。

class CItem
{
public:
  virtual int GetTanka() const;
}; 
class CHammer : public CItem
{
public:
  virtual int GetTanka() const;
  int Attack();
}; 
class CPotion : public CItem
{
public:
  virtual int GetTanka() const;
  int Drink();
};
 
void Sell(CItem c)
{
  c.GetTanka();
  c.Attack();  // error!

 ある基礎データクラスを作成し、そのクラスを継承させて、すべてのデータに対して一定の操作をしたい場合があります。

 上記の例では、アイテムは必ず CItem を継承するようにしています。こうすることで、アイテムの売買を1つの処理で記述できます。

 そのクラスが CHammer だろうと CPotion だろうと、CItem を継承している限り、売買処理を記述するのは1ヶ所だけで済みます。

 ところがこの構造が裏目に出る場合があります。売買処理の中に、あるクラスだけに発生する特殊な例外処理を記述したくなったとしてもできません。上記の例のように、もし売ろうとしているアイテムが武器ならば、残された攻撃力に応じて価格を変動させたいようなことがあるかもしれません。

 こうした特殊処理は、できるならば仮想関数をうまくつかって対処できればそれにこしたことはないのですが、規模が大きくなり処理が複雑になってくるとそういうわけにもいかなくなってきます。あるクラスの事情のためだけに、数百に及ぶほかのクラスを書き換えるのは現実的ではありません。

 そこでC++ではインスタンスをスーパークラスからサブクラスにキャストするための構文 dynamic_cast が用意されています。

void Foo(CItem *item)
{
  CHammer  *pItem = (CHammer *)item;  //コンパイルは通るけど、本当に正しい?
}

 dynamic_cast を使ってキャストすれば、ヌルポインタかどうかを確認することで正しくキャストされたかどうか、つまりスーパークラスをサブクラスに変換できるかどうかを確認することができます(上記)。

void Foo(CItem *item)
{
  CHammer  *pItem = (CHammer *)item;  //コンパイルは通るけど、本当に正しい?
}

 dynamic_cast を使用しなくても、C言語のキャストを使用することで無理やりサブクラスにキャストすることができますが、これは安全ではありませんし、そのサブクラスへのキャストが正当なものかどうか判断するためには、まわりくどい仕組みを用意してやらなければならなくなるでしょう(上記)。

 ダウンキャストはたいへん強烈な印象を与える仕組みですので、変わった挙動に目を奪われがちですが、dynamic_cast が用意された本来の目的は、安全なコードを書くためだということを忘れないで下さい。C言語形式のキャストでもダウンキャストは可能ですが、dynamic_cast を使用することでコードの安全性が向上するのです。

 最後に残るキャストは reinterpret_cast という、なんとも複雑な名前のキャストです。これは主に全く異なる形式のポインタを変換するために使用するためのものです。

 C++のキャストは、キャストの目的を明確にします。const_cast なら const を外すことを目的に、static_cast なら基本的な型変換を目的にしていることが一目でわかり、コードの可読性があがります。

 こうしてC++で拡張されたキャストはいずれも直接的にプログラムの流れを制御するようなものではありませんから、これらを用いずともプログラムは作れるのですが、品質のよいプログラムを効率よく製作するためにこうした仕組みを積極的に利用すべきです。コメントをきちんと書いたり、assert を入れたりするのと同じように、キャストもC++方式を採用すれば、品質はきっと向上するはずです。


プロキシと参照カウンタ

2005-07-21 00:26:03 | C++

 プログラムの規模が大きくなると、クラスが思うように分割できず肥大化したり、パフォーマンスをあげるために美しくない構造を選択せざるを得なかったりするなど、様々な問題が発生します。これらの問題はたいてい、すでに誰かが体験し、さらに誰かが良い案を思いついていたりするものです。それをいちいち仕事の度に再発明しいていたのでは効率が悪くてしょうがありませんから、数多く書籍や口コミで広まっているデザインパターンを拾い集めたいものです。


 今回はそんなデザインパターンのひとつであるプロキシについて解説します。



CString
{
 private:
  char *m_pStr;
 public:
  CString(const char *str);
  ~CString();
};


CString::CString(const char *str)
{
 m_pStr = new char[strlen(str)+1];
 strcpy(m_pStr, str);
}


CString::~CString()
{
 delete []m_pStr;
}


 文字列を管理する CString クラスを考えてみましょう。上記はとりあえず文字列を保存するだけの簡単なものです。



 いきなり話が脱線しますが、このクラスは、コピーされるとメモリを破壊するバグを持っています。コピーコンストラクタを定義しなかった場合、コンパイラは自動的にコピーコンストラクタを作成します。これをデフォルトコピーコンストラクタと呼びます。


 デフォルトコピーコンストラクタはただメンバ変数をコピーするだけなので、たいていのクラスではクラッシュします。例えば上記の例では、m_pStr をそのままコピーしてしまうので、コピーがデストラクトされるとオリジナルはクラッシュします。これを回避するためには、下記のようにコピーコンストラクタを明確に定義します。基本的にコピーコンストラクタのないクラスは、デフォルトコピーコンストラクタでバグると考えてまず間違いありません。



CString::CString(const CString &str)
{
 m_pStr = new char[strlen(str.m_pStr)+1];
 strcpy(m_pStr, str.m_pStr);
}


 同様に、代入演算子でも同じ問題が発生します。これも代入演算子をオーバーライドしてやればいいのですが、自分で自分に代入するとやはり誤動作しますので、自分(this)にはコピーしないようにチェックする必要があります。



CString &operator=(const CString &str)
{
 if (this != &str)
 {
  m_pStr = new char[strlen(str.m_pStr)+1];
  strcpy(m_pStr, str.m_pStr);
 }
 return *this;
}


 最初のクラスでは、オブジェクトをコピーするとデータも必ずコピーされてしまいます。変更が発生するまではひとつのデータを参照するようにしたいときには、どうしたらよいのでしょうか。これを真面目に記述するのは一苦労しそうですが、ラクに解決する方法があります。


 データの管理はミニクラスにまかせ、CString はそのミニクラスへのポインタをやりとりすることで、実際にはひとつしかないデータを複数の CString から参照するようにします。このミニクラスを、慣習的にレップ(REP)と呼びます。


 また CString はREPに対するプロキシ(PROXY)である、と言います。本当にデータを管理しているのはREPで、CStringはREPの代理人(PROXY)というわけです。



class CString
{
 private:
  class CRep
  {
   private:
    char *m_pStr;
   public:
    CRep(const char *str);
    ~CRep();
    const char *str() const
     { return m_pStr; }
  };
  CRep *m_pRep;
 public:
  CString(const char *str);
  CString(const CString &str);
  ~CString();
  CString &operator(const CStrng &str);
};


CString::CString(const char *str)
{
 m_pRep = new CRep(str);
}


CString::CString(const CString &str)
{
 operator=(str);
}


CString::~CString()
{
 delete m_pRep;
}


CString &CString::operator=(const CString &str)
{
 if (this != &str)
  m_pRep = new CRep(str.m_pRep->str());
}


CString::CRep::CRep(const char *str)
{
 m_pStr = new char[strlen(str)+1];
 strcpy(m_pStr, str);
}


CString::CRep::~CRep()
{
 delete []m_pStr;
}


 上記の例は、とりあえず最初の段階として、先述の CString をそのままプロキシ型に書き換えたものです。ごく単純なプロキシの例で、目的を果たすにはまだ未完成です。下ではこれに参照カウンタを実装していきます。



class CRep
{
 private:
  char *m_pStr;
  int m_nCount;
 public:
  CRep(const char *str);
  ~CRep();
  const char *str() const;
  void AddRef();
  void Release();
};


CRep::CRep(const char *str)
: m_nCount(0)
{
 m_pStr = new char[strlen(str)+1];
 strcpy(m_pStr, str);
}


CRep::~CRep()
{
 delete []m_pStr();
}


void CRep::AddRef()
{
 m_nCount++;
}


void CRep::Release()
{
 if (--m_nCount < 1)
  delete this;
}


 参照カウンタとは、オブジェクトを参照しているオブジェクトが外部にいくつ存在するのかを、オブジェクト自身がカウントしておくというものです。参照者の増減を記録し、誰からも参照されなくなったら自ら消滅するのです。Windows で DLL を利用したことがある人ならご存じでしょう、COM のアレをクラスでやるのです。


 下記は参照カウンタを実装した CString クラスです。CRep は CString の入れ子クラスにしておきたかったのですが、見づらいので独立したクラスにしてみました。



class CString
{
 private:
  CRep *m_pRep;
 public:
  CString(const char *str);
  CString(const CString &str);
  ~CString();
  CString &operator=(const CStrng &str);
  CString operator=(const char *str);
};


CString::CString(const char *str)
{
 m_pRep = new CRep(str);
 m_pRep->AddRef();
}


CString::CString(const CString &str)
{
 operator=(str);
}


CString::~CString()
{
 m_pRep->Release();
}


CString &CString::operator=(const CString &str)
{
 if (this != &str)
 {
  str.m_pRep->AddRef();
  m_pRep->Release();
  m_pRep = str.m_pRep;
 }
}


CString &CString::operator=(const char *str)
{
 m_pRep->Release();
 m_pRep = new CRep(str);
 m_pRep->AddRef();
}


 CString クラスは、CRep への参照を得るたびに AddRef を呼び出し、CRep に参照していることを知らせます。参照を放棄するときは、Release を呼び出します。CRep::Release の中に delete this; が含まれていることに注意して下さい。CRep は必ず new でインスタンス化されなければならないのです。そして CString は、直接 m_pRep を delete せず、Release メソッドにその管理をゆだねています。


 一見危険に見えますが、CRep が外部で使われないように隠蔽しておけば問題はありません。CString はどのような使われ方をしても安全なのです(バグがない限り)。



CString original;
CString *pCopy1, *pCopy2;
pCopy1 = &original;
pCopy2 = pCopy1;
delete pCopy1;
pCopy2->empty();


//確かに参照カウンタを狂わせることには成功したけど、それ以前に何か問題があるんじゃないのか?



 もちろん、CString を記述するには、すでに説明したデフォルトコピーコンストラクタやデフォルトコピーオペレータの問題を把握しておかなければなりませんが、これについては始めに説明したとおりです。


 プロキシは巨大なメモリを効率よく扱いたいような場合の他にも、内部のデータをリストなどの複雑なポインタリンクで管理したい場合や、特定のハードウェアに対する操作を隠蔽したい場合などにも使えます。その場合、参照カウンタと必ず一組では使わないかもしれません。逆に参照カウンタを使いたい場合は、効率などとは無関係にプロキシを使うことになるでしょう。


 いかがでしょう。わりと曲芸的な設計ですよね。この曲芸的な設計はセオリーの1つであり、多くの人が採用しています。その証拠にこの設計はプログラミング言語C++第3版にも掲載されています。


 こうした設計のセオリーは、説明を容易にするため、あるいは車輪の再発明を防止するため、デザインパターンと呼ばれている一種のカタログにまとめられています。デザインパターンにあるプロキシには参照カウンタがなく一般化されていますが、代表となるクラスが実態のコピーを管理すると言う基本構造は共通です。


 オブジェクト指向言語でプログラミングしている人々の間ではデザインパターンは常識です。今回説明した曲芸的な設計でも、デザインパターンを知っている人であれば、「プロキシパターンで実態に参照カウンタを持たせている」とだけ言えば、わざわざ説明するまでもなく理解できてしまうのです。


 今回デザインパターンの一例としてプロキシを取り上げたのは、単にデザインパターンを紹介したかっただけでなく、オブジェクト指向設計の雰囲気を掴むのに非常に適した素材であると思ったからです。


 C言語的設計から脱却できないとき、クラス設計がうまくいかないときには、このサンプルの雰囲気を思い出し、デザインパターンの本を見開いて、今作ろうとしているものに最適なパターン名がないか探してみてください。


インターフェイスと抽象基底クラス

2005-07-20 01:51:27 | C++

 インターフェイスというのはメソッドのことです。と言い切ってしまうと語弊があるのですが、C++ではクラスのインターフェイスをメソッドで表すことになっています。

 インターフェイスというのはクラスを操作する手段です。オブジェクト指向の解説では、メッセージを送受信することでオブジェクト同士が協調して動作するというイメージがよく語られますが、C++の場合はメソッドをインターフェイス(窓口)としてメッセージを送受信するわけです。そのメソッドが呼び出されたという事実そのものもクラスにとってひとつのメッセージですし、メソッドに引数があればそれもまたメッセージとなります。

class CCounter
{
 public:
  m_nCount;
  m_nMax;
  CCounter(int max);
  int Count();
  bool IsEnd() const;
};
int CCounter::Count()
{
 if (m_nCount < m_nMax)
  if (++m_nCount >= m_nMax)
   printf("count is over!n");
 return m_nCount;
}

bool CCounter::IsEnd() const
{
 return m_nCount >= m_nMax;
}

foo()
{
 CCounter cnt(100);
 cnt.m_nCount = 120;
 while(cnt.IsEnd())
  cnt.Count();
}

 インターフェイスという概念は、データ隠蔽の概念の上に成り立っています。まずはデータの隠蔽がきちっとしていなければなりません。

 オブジェクト指向ではデータの隠蔽にとやかくうるさいわけですが、その理由は単純明快です。上記の例のように、クラスのメンバ変数に外部から簡単にアクセスできてしまうようではめちゃくちゃになってしまいます。"count is over!" が出力されないまま終了してしまいますよね。

 もし隠蔽しなかった場合、そのときは都合が良くても、仕様変更したい場合や継承したい場合などにたいへん困ることになるでしょう。

 インターフェイスはメンバ変数への単純なアクセスだけでなく、メンバ変数の内容を元に加工したデータをやりとりすることもあります。例えば、上記の CCounter で IsEnd メソッドは、別に終了を意味する bool のフラグをメンバ変数に従えているわけではありません。

 メンバ変数が private であれば、カウントが確かに終了しているかどうかは実際にカウンターの値や最大値などの情報を記録している CCounter だけが判別できます。これは CCounter だけが判別すればよいというように、情報の一元管理を可能にします。このような考え方を、情報のカプセル化といいます。

 隠蔽、カプセル化、インターフェイスの3つは全く別なものではなく、それぞれがお互いに関係しあっている概念です。

 隠蔽とカプセル化がきちんとしているという前提があれば、クラスを設計するときはインターフェイスを考えるだけで済みます。具体的な実装をする前に、class の定義を作り、インターフェイス、すなわちメソッドを一通り揃えるというやりかたが一般的な設計の流れとなっています。

class CFile
{
 private:
  FILE m_Hdl;
 public:
  CFile(char *filename, char *mode);
  virtual ~CFile();
  int GetChar();
  bool IsEof() const;
};
CFile::CFile(char *filename, char *mode)
{
 m_Hdl = fopen(filename, mode);
 assert(m_Hdl >= 0);
}

CFile::~CFile()
{
 fclose(m_Hdl);
}

int CFile::GetChar()
{
 return fgetc(m_Hdl);
}

bool CFile::IsEof() const
{
 return feof(m_Hdl);
}

 CFile はファイルハンドルを隠蔽する、ありがちな例です。CFile をデストラクトすれば自動的に close されるという、たいへん有用な例と言えるでしょう。

※このクラスはある問題を含んでいますが、それは後日解説します。

int foo()
{
 int cnt;
 CFile file("test.dat", "rb");
 while(!file.IsEof())
 {
  int c = file.GetChar();
  if (c == 'r')
   return cnt;
  putc(c);
  cnt++;
 }
 return cnt;
}

 例えばこのように利用するわけです。Cでプログラムする場合は return の前に close しなければなりませんが、CFile を使えば自動的にファイルをクローズしてくれます。

int DispWithoutNumber (CFile &file)
{
 int c;
 while((c = file.GetChar()) != EOF)
 {
  if (!isnum(c))
   putc(c);
 }
}

 DispWithoutNumber は、指定されたファイルから文字を読み込んで、数字以外を表示する関数です。この関数は、CFile を継承いているクラス全てが利用できるので、汎用性も高いでしょう。

 さて、ここで特定のメモリ領域にあるデータを、同じように数字以外の文字を表示するようにしたい場合はどうしますか? メモリは fopen も fclose もできませんから、CFile は継承できません。

int DispWithoutNumberFromMemory(char *buff, int size);

 こんな新しい関数を作らなければならなくなってしまいました。となると、CFile は本当に汎用性が高かったのでしょうか?

 オブジェクト指向は再利用性の高さが売りだったはずですよね。もちろんC++でも、このような問題に対する解決方法はちゃんと用意されています。

class CStreamInterface
{
 public:
  virtual int GetChar() = 0;
  virtual bool IsEof() const = 0;
};
class CFile : public CStreamInterface
{
 private:
  FILE m_Hdl;
 public:
  CFile(char *filename, char *mode);
  virtual ~CFile();
  virtual int GetChar();
  virtual bool IsEof() const;
};
class CMemoryStream : public CStreamInterface
{
 private:
  int m_Seek, m_Size;
  char *m_pBuff;
 public:
  CMemoryStream(char *buff, int size);
  virtual ~CMemoryStream() {}
  virtual int GetChar();
  virtual bool IsEof() const;
};
CMemoryStream::CMemoryStream(char *buff, int size)
: m_Seek(0), m_Size(size), m_pBuff(buff)
{
}

int CMemoryStream::GetChar()
{
 if (IsEof())
  return EOF;
 return m_pBuff[m_Seek++];
}

bool CMemoryStream::IsEof()
{
 return m_Seek >= m_Size;
}

 内部的にかなり性質が異なる場合でも、インターフェイスが同じ場合には、純粋仮想関数だけしかない抽象基底クラスを作成します。

 純粋仮想関数というのは実装のない仮想関数で、純粋仮想関数を含むクラスを抽象基底クラスと呼びます。抽象基底クラスはインスタンス化できませんので、オブジェクトを生成するためには必ず継承しなければなりません。

 同じ抽象基底クラスを継承させることで、異なるクラスに同じインターフェイスを与えることができます。以下の DispWithoutNumber 改良版ならば、上記の CFile も CMemoryStream も変更することなく扱えるのです。これで DispWithoutNumber をいくつも作るハメになるようなことは避けられました。

int DispWithoutNumber (CStreamInterface &stream)
{
 while(!stream.IsEof())
 {
  int c = stream.GetChar();
  if (!isdigit(c))
   putc(c);
 }
}

 Java には純粋仮想関数のみで構成された抽象基底クラスと同様な仕組みを備えています。その名も interface です。

 ならば全てのクラスで抽象基底クラスを導入すべきかというと、私は今のところそうとは言い切れないと思っています。C++の継承も、Java の interface も、慎重な判断と十分な経験が必要です。

 C++にしても Java にしても、このあたりは何かと議論の種になりがちなところですがうまく使えば再利用性が高くなります。似たようなコードを何度も作っていたのでは、効率が悪いだけでなくつまらないバグを入れてしまう危険も増えます。みなさんもうまく使いこなして再利用性を高めてみて下さい。


仮想関数とポリモーフィズム

2005-07-20 01:30:56 | C++

 オブジェクト指向といえばクラスです。C++のクラスはC言語の構造体を拡張したモノなので、C言語からわざわざ乗り換える必要なんて無いんじゃないかと思われがちですよね。メンバを public と private に分離できると、それなりにデータ隠蔽できそうですけれど、あんまり大きなメリットはなさそうですし。関数をメンバに持てるというけれど、下記のようにCでも作れます。それじゃ、なんでわざわざクラスなんてモノがあるのでしょうか?

class CFoo
{
 private:
  int m_Member;
 public:
  CFoo();
  int SetMember(int num);
  int GetMember() const;
};

typedef struct
{
  int m_Member;
} SFoo;

SFooInit(SFoo *this);
int SFooSetMember(SFoo *this, int num);
int SFooGetMember(SFoo *this);

 継承というのはクラスのひとつのポイントですが、再利用生が高くなるとはよく言われますけれど、ただ継承しただけではそうそう再利用性は高くならないものです。今回は継承のおいしい活用法である、仮想関数を利用したポリモーフィズムについて説明します。ポリモーフィズムはクラスの汎用性と再利用性をあげるための主力となる概念のひとつです。

typedef struct
{
 int job;
 char *name;
 int hp;
 int max_hp;
 int level;
} stHuman;

void HumanInit(stHuman *man, int job, char *name)
{
 static const int def_hp[3] = { 120, 80, 100 };
 assert(job >= 0 && job < 3);
 man->job = job;
 man->name = name;
 man->hp = man->max_hp = def_hp[job];
 man->level = 1;
}

 まずはC言語で作ってみることにしましょう。RPGのキャラクターを管理するための構造体を作ってみます。HumanInit 関数は stHuman を初期化するための関数です。内容は何となくわかるでしょう。

 とりあえず今回はパラメーターも少ないので、直接値を設定しています。パラメーターが増えたら def_hp を int じゃなくて stHuman 型の配列にするとか、職業ごとに専用の初期化関数を用意するとかしてもいいでしょう。

 それではこのキャラクターを休ませる関数を作ってみましょう。レベル×10の基本回復量をベースに、ベッドのコンディションに応じた回復率を掛けることにしました。

int HumanAddHP(stHuman *man, int gain)
{
 man->hp += gain;
 if (man->hp > man->max_hp)
  man->hp = man->max_hp;
 if (man->hp < 0)
  man->hp = 0;
 return man->hp;
}

int HumanSleep(stHuman *man, float condition)
{
 return HumanAddHP(man, man->level * 10 * condition);
}

 さらに、職業に応じて回復量の計算方法を変えることにしてみます。戦士ならどんなコンディションでも十分回復できるのですが、魔法使いはコンディションの影響をもろに受けることにしましょう。

int HumanSleep(stHuman *man, float condition)
{
 switch(man->job)
 {
  case JOB_FIGHTER:
   return HumanAddHP(man, man->level * 10);
  case JOB_MAGE:
   return HumanAddHP(man, man->level * 10 * (condition*1.2));
  default:
   return HumanAddHP(man, man->level * 10 * condition);
 }
}

 このように、職業に応じた処理をする場合、switch などで処理を振り分けることになります。規模が小さいうちはよいのですが、もしこのような switch 文が数百カ所に分散していたとしたらどうでしょうか。

 switch 文による処理の振り分けを最小限におさえることはクラスを使う目的のひとつです。しかしクラスを使えば必ず switch 文が減るというものではなく、ポリモーフィズムについての知識がなければ switch 文は減りません。

class CHuman
{
 protected:
  char *m_szName;
  int m_nHP;
  int m_nMaxHP;
  int m_nLevel;
 public:
  CHuman(char *name, int maxhp = 100);
  virtual ~CHuman();
  virtual int AddHP(int gain);
  virtual Sleep(float condition);
};

int CHuman::Sleep(float condition)
{
 AddHP(m_nLevel * 10 * condition);
}

 さっそくですが、stHuman をクラス化した CHuman を作成しました。詳細については特に説明する必要はないと思います。

 stHuman と違って、CHuman には job を識別するための変数がありません。これまでは job 変数を参照して処理を振り分けていましたが、職業毎にクラスを分けて、クラス毎に自動的に処理が振り分けられるようにしてみましょう。

class CFighter : public CHuman
{
 public:
  CFighter(char *name);
  virtual int Sleep(float condition);
};

CFighter::Sleep(float condition)
{
 AddHP(GetLevel() * 10);
}

class CMage : public CHuman
{
 public:
  CMage(char *name);
  virtual int Sleep(float condition);
};

CMage::Sleep(float condition)
{
 AddHP(m_nLevel * 10 * (condition * 1.2));
}

 Sleep メソッドの内容は職業に応じて異なっています。この Sleep メソッドには virtual というキーワードが付けられていることに注意して下さい。virtual というキーワードはそのメソッドが仮想関数であることを示しています。

CInn::Stay(CHuman *man)
{
 man->Sleep(1.5);
}

 宿屋にしてもキャンプにしても、その人がどのような職業に就いているのかまではチェックしないでしょう。man は CFighter かもしれませんし、CMage かもしれませんけれど、Stay では単純に CHuman として Sleep を呼び出しています。こうすると CHuman::Sleep が呼び出されてしまうように見えますが、Sleep は仮想関数ですので、そのオブジェクトを生成したクラスの Sleep メソッドがちゃんと呼ばれます。

 このように、CHuman としてポインタをやりとりしているのに、実際に呼び出されるメソッドがオブジェクト毎に変化するような仕組みを利用したプログラミングをポリモーフィズムと呼びます。厳密に言えばポリモーフィズムにはもっと広い意味があるのですが、C++でポリモーフィズムといえばたいていはこの仮想関数を利用したコーティングスタイルのことを指します。

 もし CMage を継承して CWizard を作成しても、CInn::Stay メソッドを修正する必要はありません。CHuman を継承するときは自分のことだけを考えれば済みますし、CHuman を利用する側としては CHuman のことだけを知っていればいいのです。修正個所が少なくなるだけでなく、CInn に関係する処理は CInn の中だけ、CHuman に関係する処理は CHuman の中だけに集中しますので管理がラクになります。

 仮想関数にするメソッドは慎重に決めなければなりません。もし CHuman::Sleep が仮想関数ではなかったら、CInn::Stay は man を CHuman としてしか扱えなくなってしまいます。特にデストラクタは仮想関数にしておいたほうが無難なのですが、これはまた別の機会に説明したいと思います。


このブログについて

2005-07-20 01:19:54 | C++
以前からホームページ上でC++入門と称したページを作っていました。この入門を作成した動機は、C++の正しい使い方を意外とわかっていない人が多いと感じていたことと、上級者と初心者の間を埋めるような書籍やサイトがあまりにも少ないと思われたためです。

入門記事では機能的な仕様の説明に終始しています。なぜその機能があるのか、どのように使うのかといったところが説明されていません。むしろそこをきちんと説明すれば詳細は後からついてくるはずです。

これらの記事はもう5年以上前に作成したものです。それなりに読んでくれていたようで、一定の役目を果たすことができたのではないかと思いますが、文章的にも技術的にも稚拙なところが多々あり、そろそろ削除してもよいかと思っていました。しかし某プログラミング雑誌であまりにもダメな記事をまた見てしまい、ここにブログとして復活させようと考えた次第です。

なお転載にあたり記事の一部を修正してあります。オリジナルの記事がほしいと思う人はいないと思いますが…。