変数名に “g_” や “m_” をつけることの意味

2020年1月20日

C++ の一般的なコーディングスタイルの一つに、変数名にプレフィックスを付けるというものがあります。細かい点ではチームによってぶれがあるでしょうが、おおむねグローバル変数とメンバ変数に何かを付けるという点では一致しているようです。

私もやはりこれだけは外す訳にはいかないと考えます。実際にそれなりの規模の案件を終えてみて、このルールが果たした意義は想像を超えて大きかったと感じています。

推奨ルール

寿命が一時的でない変数の頭には、たとえば次のようなプレフィックスをつけて、これらを一目で識別できるようにしておくべきです。

グローバル変数g_
メンバ変数m_

私はコーディング規約はできるだけ薄くしたいと考えるほうですが、それでもこれを強く推奨します。

ネット上のさまざまな議論を見ていると、「上手く書けばこのようなスタイルは不要になる」といった意見があることに驚きます。まるで、無灯火で自転車を走らせて「俺は見えている」と主張する高校生のようです。あなたが見るのではなく「周りが」見るのだということがわかっていない。付けることがダサいと考えている点でも同じです。

ダサいと思う気持ちはわかりますが、それは目立つということの裏返しでもあります。実際これらのプレフィックスは、ソースコードを読む人に対して強烈な光を放ってくれるものです。

グローバル変数の場合

グローバル変数に “g_" を付けるのは、古くから知られているスタイルです。

static bool g_initialized = false;
const int g_table[] = { 0, 1, 2, 3 };

Apple g_apple;
Banana g_banana;

namespace {
Cherry g_cherry;
Durian g_durian;
}

ファイルスコープの場合、プレフィックスを変えてみたり外してみたりということを考えてしまいがちですが、そのような区別にさしたる有効性はありません。ファイルスコープであろうと区別なく “g_" としておくことをおすすめします。

このプレフィックスがなぜ重要かというと、それは過去の記事でも述べた「副作用」の存在を明らかにするからです。たとえば次のコードを見てください。これは与えられた配列の平均値を返す関数です。

float calcAverage(float values[])
{
  float sum = 0;
  for (int i = 0; i < num_channels; ++i) { sum += values[i]; }
  return sum / num_channels;
}

上のコードのうち、num_channels だけがグローバル変数であることに気づくことができましたか? プレフィックスがないと、こんなに小さな関数であってさえ、副作用の存在を見抜くのが困難になります。ここに “g_" を付ければ、その存在がたちどころにわかります。

float calcAverage(float values[])
{
  float sum = 0;
  for (int i = 0; i < g_num_channels; ++i) { sum += values[i]; }
  return sum / g_num_channels;
}

そうすれば、新たな気づきが得られることもあります。たとえば上のコードを見た人が、こう言うかもしれません。「たかが平均値を求めるのに、グローバル変数を持ち込む必要はないだろう」と。それに応じると、次のようになります。

float calcAverage(float values[], int num_values)
{
  float sum = 0;
  for (int i = 0; i < num_values; ++i) { sum += values[i]; }
  return sum / num_values;
}

この関数は修正前に比べて、格段に良くなっています。汎用性が高まったうえに、単体テストが可能になっているからです。

このように “g_" は、コードの断片を見ただけで気づきを得られる効果があります。もちろん、副作用を持ち込んでいることをコードを書く本人が自覚できるという効果もあります。このようなメリットを排除して開発を進めることは、もはや考えられないことです。

メンバ変数の場合

メンバ変数の場合も、鍵となるのは副作用です。たとえば、次のようなメンバ関数がなぜ const 修飾されていないのだろうと疑問に思うことが、たまにあります。

float SpeedMeter::calcVelocity()
{
  float pos_current = getPositionCurrent();
  float velocity = (pos_current - pos_previous) / 1.0e-3F;
  pos_previous = pos_current;
  return velocity;
}

ここでの pos_previous がメンバ変数であるということは、目を皿のようにしてコードを見ればわかりますが、関数がさらに長かったとしたらどうでしょう。

メンバ変数に “m_" を付ければ、そのような苦労は要りません。const 修飾できない理由を突き止めるのに、"m_" が付く変数への代入を探すだけで済むようになるからです。

float SpeedMeter::calcVelocity()
{
  float pos_current = getPositionCurrent();
  float velocity = (pos_current - m_pos_previous) / 1.0e-3F;
  m_pos_previous = pos_current;
  return velocity;
}

インスタンスへの副作用が5行目にあることは明らかですね。

逆に、あるメンバ関数の中で “m_" が付く変数への代入が一度もなければ、それは誰が見ても const メンバ関数にできるということが言えるため、コード改善の手掛かりになることもあります。

さらに言うと、あるメンバ関数の中で “m_" が付く変数自体が一度も登場しなければ、それは static メンバ関数にできるということも言えます。これは “m_" のルールなくしてはほぼ発見不可能なことです。

まとめ

これら2つのプレフィックスに共通するのは、「コードの断片からでも」何かが発見できるということです。他人はあなたのコードを「あなたが見るようには」見てくれないものです。そのことを考慮すると、ぼーっと眺めていても何かが目に飛び込んでくる仕掛けというのは、コードの品質を高めるうえでとても重要なのです。