9.プリプロセッサ


9−1.インクルードとリンク

ここまでの解説で

===============
#include "system.inc"
===============

の正体を明かしていました。その行に"system.inc"の内容が挿入されるのですが、重要なのは挿入されるタイミングです。 #includeに限らず、本章で説明するプリプロセッサは全てコンパイル前に処理されます。 そのため、#includeの場所に対象ファイルが文字通り(メモリ上に)コピペされてから、コンパイルが開始されます。 なお、インクルードされたファイルの中にさらに「#include」が記述されていた場合、さらにその場所に挿入されます。

ちなみに、上記のファイルは拡張子が"*.inc"になっていますが、拡張子が何であっても構いません。

===============
#link "gamecommon.spt"
===============

このリンクに関しても、解釈自体はコンパイルのタイミングで行われます。 しかしここで言う「解釈」とは「gamecommon.sptを外部参照するよ」という宣言のことなので、「gamecommon.spt」のコンパイル自体は、今読み込んでいるスクリプトがコンパイルされた後に行われます。


9−2.定数定義

例えば角度をラジアンに変換する場合、180度がπラジアンのため、以下のように計算できます。

===============
rad=45*(3.14159/180);
===============

3.14159は円周率なのですが、毎回「3.14…」と書くのは面倒だし間違えがありそうです。 そのため、解決策として考えられるのは変数に代入することです。

===============
pi=3.14159;
rad=45*(pi/180);
===============

これで円周率をpiと置き換えられました。同じような表記が何度あってもpiと書くだけです。 しかし円周率という決まった値であるのに、わざわざ変数に入れるのは無駄があります。そこで、『定数』として定義する方法があります。

===============
#define PI 3.14159;
rad=45*(PI/180);
===============

上記のように記述するとPIは変数ではなく定数となり、実行時ではなくコンパイル前にPIが3.14159という数字に置き換えられます。 コンパイル前に置き換えられるため、無駄な代入処理なども発生しません。 値一つではなく、式を記述することも可能です。

===============
#define RD (3.14159/180);
rad=45*RD;
===============

「RD」が「(3.14159/180)」に置き換えられ、「rad=45*(3.14159/180)」となります。式の中身が置き換えられることを『定数が展開される』と呼ぶことがあります。 定数の定義の中に別の定数を使うことも可能です。

===============
#define PI 3.14159;
#define RD (PI/180);
rad=45*RD;
===============

RDの定義にカッコを使っていますが、これは意図しない演算子の優先順位の逆転が起こらないようにするために必要です。 #defineが単なる置き換えであるために例えば以下のように記述してしまうと意図しない結果になってしまいます。

===============
#define X 3+6       //  Xが「3+6」なので9になるはず
val=X*4;            //  9*4で36になるはず
===============

ところが「val=3+6*4」と置き換えられるので結果は27になってしまいます。そこで以下のようにカッコで囲うことで意図したとおりに計算されるようになります。

===============
#define X (3+6)     //  Xが「(3+6)」なので9になるはず
val=X*4;            //  「val=(3+6)*4」と展開されるので36になる
===============

掛け算ならばカッコは要らないように思えるかもしれませんが、要る場合・要らない場合というように考えると間違える元なので、式を定数定義する場合は必ずカッコを使う、というように覚えておくと良いです。

※C言語には#defineによるマクロ関数が存在しますが、SphereScriptにはマクロ関数はありません。


9−3.条件付き解釈

実用的なプログラムを書こうとすると、「デバッグ時のみ実行してほしい処理」というものが必ず出てきます。

===============
...
x=get_stat(id);
print_string("x=" + x + "\n");
if (x>9) {
...
===============

こういう場合、デバッグ用定数を用意してif文で分けることが可能です。

===============
#define DEBUG 1
...
x=get_stat(id);
if (DEBUD) {
    print_string("x=" + x + "\n");
}
if (x>9) {
...
===============

こうすることで、デバッグ時はDEBUGに1を入れて、デバッグが終わったらDEBUGには0を入れればデバッグ用処理は実行されません。 ただ、デバッグが終わって使わないことが明らかなのにわざわざif文の条件判断を行うのは無駄です。そこで、以下のように記述すると、コンパイル前に不要な処理を除去する事が可能です。

===============
#define DEBUG 1
...
x=get_stat(id);
#if DEBUD!=0
    print_string("x=" + x + "\n");
#endif
if (x>9) {
...
===============

DEBUGが0でない場合、#if〜#endifまでの間がコンパイルされ、DEBUGが0ならばコンパイルされずに除去されます。 通常のif文と異なるのはif文が実行時に判断されるのに対して、#ifはコンパイル前に判断されることです。 コンパイル前の処理されて展開されてしまうため、いくら#ifを使っても計算が重くなることがありません。

上記の例では「DEBUD!=0」と表記していますが、「DEBUG>0」とか「MODE*3 >= 10」というよう式を記述することが出来ます。単に「#if DEBUG」と書くことも出来ます。この場合は定数DEBUGが真か偽か判断されます。 注意したいのが、#ifはコンパイル前に解釈されるため、変数を使うことが出来ません。以下のようなプログラムはコンパイルできません。変数はコンパイル後に実行を始めてから作成されるため、コンパイル前の#ifでは変数は存在しないことになるからです。

===============
#define DEBUG 1
...
x=get_stat(id);
#if x>0           //  ここでコンパイルエラー発生
    print_string("x=" + x + "\n");
#endif
===============

if文にelseがあるのと同様に、#ifにも#elseが存在します。「else if () ...」という表記と同様に#elseifで実現可能です。

===============
#if DEBUG_MODE == 0
    //  DEBUG_MODEが0のときにコンパイルされる処理
#elseif DEBUG_MODE == 1
    //  DEBUG_MODEが1のときにコンパイルされる処理
#else
    //  DEBUG_MODEが0と1以外のときにコンパイルされる処理
#endif
===============

#ifはネスト構造に出来ます。

===============
#if DEBUG_MODE == 0
    //  DEBUG_MODEが0のときにコンパイルされる処理
    #if MOVIE_TYPE == 0
        //  DEBUG_MODEが0で、MOVIE_TYPEも0のときにコンパイルされる処理
    #endif
#endif
===============

それぞれの#ifに対して、最後の#endifを忘れないようにしてください。

#ifの次には判定式が入るわけですが、#ifdefという構文を使うことで「定数が定義されているかどうか」を判定することも出来ます。 以下のように記述してください。

===============
#define DEBUG
#ifdef DEBUG
    //DEBUGが定義されている場合のみコンパイルされる処理
#endif
#ifdef DEMO_MODE
    //DEMO_MODEという定数はどこでも定義されていないのでコンパイルされない
#endif
===============

「定数が定義されていない」ことを判定するためには#ifndefを使用します。

===============
#ifndef DEMO_MODE
    //DEMO_MODEは定義されていないのでコンパイルされる
#endif
===============

判定するのが「定義されているかどうか」だけなので#ifよりも使い勝手が悪いようですが、実際は#ifdefのほうが使う場面が多いです。具体的にはインクルードファイルです。 インクルードファイルはいろいろなソースコードファイルからインクルードされますが、入れ子構造でインクルードする場合、同じファイルが何度もインクルードされてしまうことがあります。 同じ記述が何度もコピペされてしまうため、正しくコンパイルされません。インクルードの順番や記述を見直すことで解決できないこともないですが、科学的とはいえないため、インクルードファイルの先頭と最後に以下のように記述します。

===============
#ifndef __SYSTEM_INC__
#define __SYSTEM_INC__

... //インクルードファイルの中身

#endif
===============

こうすると、最初にインクルードされた場所では__SYSTEM_INC__が定義されていないので#ifndefの中身がコンパイルされます。 ところが#ifndefのすぐ下で__SYSTEM_INC__を定義していますので、2回目のインクルードでは#ifndefの中身はコンパイルされません。結果的に「何度インクルードしても最初のインクルードのときだけ解釈されるファイル」を作ることが出来ます。 大変便利なので、インクルードすることを前提とするファイルでは必ずこの記述をつけるようにするといいでしょう。 当たり前ですが、別のファイルで__SYSTEM_INC__を定義してしまうと意味が無くなってしまうので、ファイル名に応じて定数名変えることが基本です。 アンダーバーなどを前後に配置しているのもこのためです。単に「#ifndef SYSTEM」などとしてしまうと、他の場所でSYSTEMを定数定義してしまうかもしれませんからね。

今まで解説した記述「#include/#define/#if/#endif/#else/#elseif/#ifdef/#ifndef」のほかに、「#elifdef/#elifndef」も使用できます。 それぞれ#elseifの条件式の代わりに「定数が定義されているか/されていないか」を条件に取るものです。

なお、#で始まる一連の置き換えや条件判断の構文を『プリプロセッサ』と呼びます。 SphereScriptのプリプロセッサはC言語のそれを模しています。 「#elifdef/#elifndef」だけはC言語に存在せず、また「何で無いんだろう?」とよく言われてるものだったため、用意しました。普段は使わないと思いますが覚えておくといざというとき役に立つかもしれません。


最初のページへ戻る