ブログ 約6分

Doom 3 は「シンプルに保て」が機能することの証明である

Share this article
Doom 3 は「シンプルに保て」が機能することの証明である

最高のC++ソースコードをWebで検索すると、Doom3のソースコードが何度も挙がり、次のようなコメントが見つかります。

Doom3のソースコードを少し調べてみました。おそらく、私がこれまでに見た中で最もクリーンで美しいコードです。

Doom 3は、 id Software が開発し、 Activisionが販売したビデオゲームです。このゲームは350万本以上を売り上げ、id Softwareにとって商業的な成功となりました。

2011年11月23日、id Softwareはこれまでの伝統を守り、旧エンジンの ソースコード を公開しました。このソースコードは多くの開発者によってレビューされました。たとえば、Fabienによる次のフィードバック(原文)があります。

Doom 3 BFGはC++で書かれています。C++は非常に広大な言語であり、優れたコードを生み出すことも、目を背けたくなるような代物を生み出すこともできます。幸い、id Softwareは「C with Classes」に近いC++サブセットに落ち着き、頭にすんなり入ってくるコードになっています。

  • 例外を使わない。
  • 参照を使わない(ポインタを使う)。
  • テンプレートの使用は最小限。
  • const を至る所で使用。
  • クラス。
  • ポリモーフィズム。
  • 継承。

多くのC++専門家は、もはや「C with Classes」アプローチを推奨していません。しかし、Doom3は2000年から2004年にかけて開発されたため、モダンC++の仕組みが使われていないのも納得できます。

それでは、 CppDepend を使ってそのソースコード内部を見て、何がこれほど特別なのかを探ってみましょう。

Doom3は少数のプロジェクトによってモジュール化されています。各プロジェクトの一覧と、その型に関する統計を次に示します。

doom5

次は、それらの関係を示す依存関係グラフです。

doom2

Doom3では多くのグローバル関数が定義されています。ただし、大半の処理はクラス内に実装されています。

データモデルは構造体を使って定義されています。上のメトリクスビューでは、構造体が青い長方形として示されており、ソースコード内でどう使われているかを具体的に把握できます。

Metric View では、コードベースはツリーマップとして表現されます。Treemapping は、入れ子になった長方形を使ってツリー構造のデータを表示する手法です。ここで使用されるツリー構造は、通常のコード階層です。

  • プロジェクトは名前空間を含む。
  • 名前空間は型を含む。
  • 型はメソッドとフィールドを含む。

doom13

ご覧のとおり、多くの構造体が定義されています。たとえば、DoomDLLの型の40%以上が構造体です。これらはデータモデルを定義するために体系的に使われています。この手法は多くのプロジェクトで採用されていますが、マルチスレッドアプリケーションでは大きな欠点があります。公開フィールドを持つ構造体は不変ではないからです。

不変オブジェクトを使うことには、重要な利点が1つあります。並行プログラミングを劇的に単純化できるのです。考えてみてください。なぜ正しいマルチスレッドコードを書くのは難しいのでしょうか。それは、リソース(オブジェクトやその他のOSリソース)へのスレッドアクセスを同期させるのが難しいからです。なぜこれらのアクセスを同期させるのが難しいのでしょうか。複数のスレッドが複数のオブジェクトに対して行う多数の読み書きアクセス間で競合状態が発生しないことを保証するのが難しいからです。では、書き込みアクセスがなくなったらどうなるでしょうか。言い換えれば、スレッドからアクセスされるオブジェクトの状態が変化しなければどうなるでしょうか。その場合、同期はもう必要ありません。

少なくとも1つの基底クラスを持つクラスを検索してみましょう。

doom6

構造体とクラスの約40%に基底クラスがあります。OOPにおいて、継承の主な利点の1つはポリモーフィズムです。ソースコードで定義されている仮想メソッドを青で示します。

doom7

メソッドの30%以上が仮想メソッドです。純粋仮想メソッドは少なく、定義されているすべての抽象クラスの一覧は次のとおりです。

doom9

定義されている抽象クラスは52個だけで、そのうち35個は純粋インターフェース、つまりすべての仮想メソッドが純粋仮想です。

doom8

RTTIを使用しているメソッドを検索してみましょう。

doom17

RTTIを使用しているメソッドはごくわずかです。

要約すると、使われているのは基本的なOOP概念だけです。高度なデザインパターンはなく、インターフェースや抽象クラスの多用もなく、RTTIの使用も限定的で、データは構造体を使って定義されています。

ここまでの内容だけでは、このコードが、多くのC++開発者から批判される「C with Classes」を使ったほかの多くのコードと特に違う点は見えてきません。

このコードの成功を説明する、開発者による興味深い選択をいくつか紹介します。

1. 有用なサービスを備えた共通基底クラスを提供する

多くのクラスが idClass を継承しています。

doom10

idClass は次のサービスを提供します。

  1. インスタンス生成。
  2. 型情報管理。
  3. イベント管理。
doom11

2. 文字列操作を容易にする

一般に、文字列はプロジェクトで最も広く使われる型の1つです。多くの操作が文字列に関係するため、それらを操作する関数が必要になります。

Doom3では idStr クラスが定義されており、文字列を操作するために必要な有用なメソッドがほぼすべて含まれています。ほかのフレームワークが提供する多くの文字列クラスのように、独自メソッドを定義する必要はありません。

3. ソースコードがGUIフレームワーク(MFC)から高度に分離されている

MFCを使う多くのプロジェクトでは、コードがその型と強く結合しており、MFCの型がコード内の至る所に見つかります。

Doom3では、コードがMFCから高度に分離されています。次のCQLinqクエリが示すように、直接依存しているのはGUIクラスだけです。

doom3

この選択は生産性に大きな影響を与えます。実際、MFCフレームワークを扱う必要があるのはGUI開発者だけで、ほかの開発者はMFCに時間を費やす必要がありません。

4. 非常に優れたユーティリティライブラリ(idlib)を提供している

ほぼすべてのプロジェクトで、最もよく使われる型はユーティリティクラスです。それは次のクエリ結果にも表れています。

doom4

ご覧のとおり、最もよく使われているのはユーティリティです。C++開発者が優れたユーティリティフレームワークを使わない場合、開発時間の大半を技術レイヤーとの格闘に費やすことになります。

idlibは、文字列、コンテナ、メモリを扱うために必要なすべてのメソッドを備えた便利なクラスを提供し、開発者の作業を容易にして、ゲームロジックにより集中できるようにします。

5. 実装が非常に理解しやすい

Doom3はハードコードされたコンパイラを実装しています。C++開発者なら分かるように、パーサーやコンパイラを開発するのは簡単ではありません。しかし、Doom3の実装は非常に理解しやすく、コードも非常にクリーンです。

コンパイラが使用するクラスの依存関係グラフを次に示します。

doom16

次に、コンパイラのソースコードからのスニペットを示します。

doom15

私たちはこれまで、多くのパーサーとコンパイラのソースコードを研究してきました。しかし、これほど理解しやすいソースコードを持つコンパイラに出会ったのは初めてです。Doom3のソースコード全体についても同じことが言えます。まさに魔法のようです。Doom3のソースコードを探索すると、思わず「すごい、美しい!」と言いたくなります。

まとめ

Doom3の設計上の選択は非常に基本的なものですが、設計者は開発者がゲームロジックにより集中し、技術レイヤー全体を扱いやすくするために多くの決定を下しており、それが生産性を大きく高めています。

ただし、「C with Classes」を使う場合は、自分が何をしているのかを正確に理解していなければなりません。Doom3の開発者のようなエキスパートである必要があります。初心者がモダンC++の推奨事項を無視してリスクを冒すことは推奨されません。

Share this article