ゲームプログラマのための設計シリーズ:原理・原則編の記事です。
概要
- 「その変数が現在の値に至るまでに確認すべき事象の多さ」を考えてみよう
- それゆえ、ローカル変数/const変数を優先しよう
本文
今更ですが、C++において変数のスコープはグローバル・メンバ・ローカルの3種類ありますね。
そして、メンバ変数・ローカル変数にはそれぞれにストレージクラス(static/非static)の分類が付きます。
さらに、それぞれconst修飾子が付きます。
// グローバルスコープ
int global; // グローバル変数
const int cglobal = 0; // constグローバル変数
// クラススコープ
class C
{
int mMember; // メンバ変数
const int cMember = 0; // constメンバ変数
static int sMember; // 静的メンバ変数
static const int scMember = 0; // 静的constメンバ変数
};
// ローカルスコープ
void f()
{
int local; // ローカル変数
const int clocal = 0; // constローカル変数
static int slocal; // 静的ローカル変数
static const int sclocal = 0; // 静的constローカル変数
}
設計の観点では、
スコープ:ローカル > メンバ > グローバル
ストレージ:非static > static
const性:const > 非const
の順に性質が良いです。それには、「その変数が現在の値に至るまでに確認すべき事象の多さ」 が関係しています。
一番厳しいケースのグローバル変数では、
int global; // グローバル変数
void func0()
{
if (...)
{
++global;
}
else
{
global = 0;
}
}
void func1()
{
for (...)
{
global += i:
}
}
globalの値の変化を調べるには、プログラムが起動して以来すべてのfunc0/func1の呼び出しを追いかける必要があります。しかも、globalを触る関数はどこでも誰でも簡単に増やせてしまいます。これは大変ですね。
(private)メンバ変数では、
class C
{
int mMember; // メンバ変数
void func0()
{
if (...)
{
++mMember;
}
else
{
mMember = 0;
}
}
void func1()
{
...; // mMemberを使った何かしらの処理
}
};
このクラスのインスタンスが生成されて以降の、すべてのfunc0/func1の呼び出しを追いかけることになります。mMemberを触るにはこのクラスのメンバ関数でなければならないので、グローバル変数より幾分かましになりました。
一方ローカル変数では
void func()
{
int local = ...; // ローカル変数
if (...)
{
++local;
}
else
{
local = 0;
}
}
これまで何回funcが呼ばれたか考える必要もないですし、今後コードが追加されようともfuncの中を見れば済むことに変わりはなく、優しいです。
最後に、constであれば初期化された部分を読めばいいだけなので最高ですね。
この厳しい・優しいを独断で点数付けしてみましょうか。
静的const変数 | 0 |
constローカル変数/constメンバ変数 | 1 |
ローカル変数 | 10 |
メンバ変数 | 100 |
グローバル変数 | 1000 |
グローバル変数がやり玉に挙げられることが多いですが、メンバ変数もなかなか曲者であるということを認識いただければと思います。
この点数の高い危険な変数をいかに減らすか・危険な変数に触る関数をいかに減らすか という目標のために設計があると言っても過言ではありません。それゆえ、一番最初に取り上げさせていただきました。
次回は、関数側について着目してみましょう。
後編