どんなプログラミング言語でも,学習を進めていくとどこかに乗り越えないといけない壁が存在するものです。C++ではそれがクラスであることが多いようです。特にC++の場合は,ずぶの初心者ではなく,C言語を一通り使いこなした後でC++に移行してくる人が多いというほかのオブジェクト指向プログラミング言語には無い特徴があります。なまじC言語の知識があるため,オブジェクト指向と手続き型の考え方の違いから,クラスで行き詰まってしまうわけです。

 では,どうしてクラスは壁となるのでしょうか。その理由は大きく2点あると思います。一つ目は,C++が登場するまで一般のプログラマにほとんどなじみが無かったオブジェクトという概念を理解しなければならないことです。私が知っている限り,C++が登場したころ,C言語からC++への移行がスムーズにできなかったエンジニアが大勢いました。そうした人の大半は,新しく入ってきたオブジェクトという概念を受け入れられず,昔ながらのプログラミング手法にこだわってしまったために,クラスの存在意義を最後まで認めることのないまま,C++への移行を断念してしまったのです。

 2点目は,C++の多機能さゆえにもたらされた煩雑性です。新しい言語に取り組もうとしたときにエンジニアは,どうしても目新しい機能をすべて使いこなそうとしてしまいがちです。C++にはポリモーフィズム(多態性)*1と呼ばれる概念の実装の一つとして,オペレータ・オーバーロード*2の機能もあれば,クラスの多重継承*3もあります。テンプレート*4という仕様も盛り込まれています。

 これらの機能をオブジェクト指向プログラミングに慣れていない人が,最初から使いこなそうとすると,クラス同士の関連が複雑になりすぎて,極めて柔軟性にかける構造になりがちです。そうした状態でプログラミングを進めると,いずれクラス同士の関係に無理が生じて,設計のやり直しや,最悪の場合はプロジェクトそのものが暗礁に乗り上げるという事態を招くことになるのです(笑い事ではなくこういう事例は結構多いのですよ!)。するとクラス設計がトラウマになって,心理的にC++を受け付けなくなってしまいます。

プログラミング対象をより自然に オブジェクトとしてとらえる

 では,どうすれば無理なくクラスを導入できるのでしょうか。私は,すべてをオブジェクト指向で考えるのではなく,自分のスキルに見合った部分で,オブジェクト指向プログラミングと従来型プログラミングの妥協点を見つけることこそ,C++でクラスを用いたプログラミングを続けていくための最大のポイントであると考えています。そして,自分自身のスキル向上と,オブジェクト指向的な考え方への慣れ具合を見極めながら,より本格的なクラス設計に段階的に挑戦していければ,ハイスキルなC++エンジニアへステップアップできるはずです。

 例えば,C++では,どのクラスにも属さないグローバルな変数や関数を定義できます。これに対して,すべての変数や関数をいずれかのクラスに属するように設計するべきだという意見もありますが,私は,グローバルにしたほうが,より自然に感じられ,後々の拡張も容易だと考えたなら,そうしても構わないと思います。

 初学者がクラス設計を考えるうえでは,現実世界をより自然にオブジェクトとしてとらえることが重要です。一般的なオブジェクト指向の教科書ではしばしば,果物の一般的な特徴を備えた「フルーツ・クラス」を親クラスとして,その子クラス(派生クラス)として「リンゴ・クラス」「バナナ・クラス」「ミカン・クラス」などを定義する例が紹介されています。乗り物クラスの派生クラスとして,船クラスや自動車クラスを定義する例もよく見かけます。

 こうした例はわかりやすそうですが,プログラミングに結びつけることが難しいため,プログラマにとっては逆に非現実的な例と言えます。学習者がオブジェクトの具体的なイメージを描くことができず,「で,結局クラスって何?」とか,「本当にクラスって必要?」というネガティブな結論に落ち着いてしまう場合があるのです。

 そこで今回はフルーツ・クラスの例より,もう少し具体的で,それでいて実践的である,アルゴリズム対戦型「3目並べゲーム」の作成をサンプルに,オブジェクトの定義とクラスの実装(インプリメント)を説明します。3目並べゲームは,3×3のマス上に交互に黒白の石を置いていき,先に縦横斜めのいずれかに石が三つ並べば勝ちという単純なルールのゲームです*5

 プログラムを解説するには手ごろな規模ですし,3目並べゲームという現実世界を単純化して考えることで,言語としてのC++に入りやすくなるのではないでしょうか。また,このサンプル・プログラムは,プレーヤーの思考ルーチンを自由に取り替えできるように設計しました。つまり思考ルーチン作者同士でアルゴリズムの良し悪しを決める対戦が可能で,技術者心を刺激する作りになっています。

 私たちが現実世界で3目並べゲームを行う場面を思い描いたとき,対局を行う盤が必要であることにすぐに気がつきます。それから対局を行う人(プレーヤー)が2名必要なこともわかります。ただしそのプレーヤーは,ゲームへの参加資格として少なくとも3目並べを行うだけの頭脳が備わっていなくてはなりません。そのほかに,ゲーム進行を手助けする役割の人や,プレーヤーを召集して盤の前に座らせる役(コーディネータ)も,必要になるでしょう(図1)。

図1●3目並べの対局場のイメージ
図1●3目並べの対局場のイメージ

 C++のクラス設計の段階では,図1に出てくるモノをそれぞれ,オブジェクトとして定義していきます。「対局に使う石もオブジェクトではないのか?」といった議論もありそうですが,今回のプログラムはゲーム中で使用する道具としての盤機能を定義しているおり,盤セットの商品管理を行うわけではないので,そこまで小さい単位のオブジェクトを定義する必要はありません。

 図1のイメージを,C++のクラスおよび関数にブレークダウンすると表1のようになります。以下ではそれぞれのクラスについて,C++習得の壁になりそうな点を指摘しつつ,実装を考えていくことにしましょう。

表1●図1から考えられるクラス
表1●図1から考えられるクラス

盤クラスの実装―― オーバーライド,オーバーロード

 最初は,3目並べゲームの盤を管理する盤クラス(class Board)の実装です。盤クラスは,3目並べのマスの管理,残りの空きマス数の管理,最後に打たれた石の場所を管理し,盤上に石を置いたり,盤上の石を見るためのメンバー関数*6を提供します。

 実はこの3目並べ盤というのは,従来型プログラミングで考えた場合,機能が非常に抽象的で定義しにくいのですが,C++のクラスを使って定義すると,違和感なく自然に記述できます。なお,今回紹介するプログラムは,部品的な役割のクラスは,ヘッダー・ファイル*7でクラスの定義とメンバー関数の実装をしています*8

 今回の盤クラスでは,表2のメンバー関数を考えました。これらのメンバー関数で,3目並べ盤というオブジェクトの持つべき機能をすべて網羅できているかどうかはわかりませんが,最低限の機能は満たしていると思います。ときどき,すぐに使う予定が無いという理由から,オブジェクトの基本機能を構成するメンバー関数を,実装の対象から外してしまう方がいますが,それはクラスの再利用性という考え方から多少逸脱しているように思えます。クラスの機能の実装では,「使われるかどうかは別にして,オブジェクトの最低限の機能をとりあえず組み込む」ことが大切です。事実,今回の盤クラスの実装でも,サンプル・プログラムの範囲では最終的に使わなかったメンバー関数があります。しかし将来的により強力なプレーヤー・クラスを実装するときに,必要になるであろう最低限のメンバー関数をそろえたつもりです。

表2●盤クラスのメンバー関数
表2●盤クラスのメンバー関数

 では,盤クラスのメンバー関数を説明しましょう(リスト1)。コンストラクタ*9では,盤面のマス(セル)をnew演算子で生成し*10,初期化を行っています。デストラクタ*11では,そのセルをdelete演算子により破棄しています*12。3目並べでは盤上のマスを表現するのに2次元配列を使うのが一般的かもしれませんが,今回は視覚的に2次元の空間を1次元で表現する方法の紹介を兼ねて,1次元配列m_celを使って実装しています*13

リスト1●盤クラスの定義。メンバー関数でオーバーロードを使っている
リスト1●盤クラスの定義。メンバー関数でオーバーロードを使っている [画像のクリックで拡大表示]

 今回の盤クラスではコピー・コンストラクタ*14は,デフォルトのものを使わず,オーバーライド*15する関数を自力で実装しています。というのは,盤クラスのメンバー変数には,newにより動的にメモリーを確保した領域があるため,デフォルトのコピー・コンストラクタを使ったのでは正しくオブジェクトがコピーされないからです。

 コピー・コンストラクタは,オブジェクトのコピーが発生しないのであれば実装することはありません。今回のプログラムでは,後に説明するプレーヤーの思考ルーチンを呼び出す際に,オブジェクトのコピーが必要となるため,コピー・コンストラクタを実装しています。

 ほかに,メンバー関数として,getStone( )が2種類用意されていることに注意してください。これはC++のオーバーロード機能を利用して,同じ名前の関数で複数の機能を実装したものです(カコミ記事「オペレータ・オーバーロードは危険な香り」を参照)。ここではパラメータ無しでgetStone( )が呼び出された場合は,最後に打たれた場所にある石を調べ,パラメータで明示的に場所が指定された場合は,指定された場所の石を調べる仕様にしています。ところでこの盤クラスは,派生クラスを持つことを想定していません。したがってvirtual宣言した仮想関数(後述)*16は持っていません。

オペレータ・オーバーロードは危険な香り

 C++やJavaでは関数のオーバーロードが可能です。さらにC++ではオペレータ・オーバーローディングといって,演算子,キャスト,配列の添え字にいたるまで,オーバーロードができてしまいます。ところがこの機能を(特に初心者が)多用すると,ソースコードが意味不明で解読不能の状態に陥りがちです。C++エンジニアの中には,「演算子をオーバーロードするoperatorの使用は即刻中止すべき」と,いささか過激なことを言う人もいるほどです。

 これはC言語で言うところのgoto論議にも似ています。私は言語仕様の中の悪評高い部分でも,使い方次第で効果的な活用ができるし,それを使いこなすのがエンジニアの技量だと考えています。したがって,エンジニアが自らプログラム言語の1文法を,強制的に切って捨てるような制約を設けることに強く反対している立場です。しかし,それでもoperatorの使用には十分気をつける必要があると思っています。