const を使いこなそう

2020年1月15日

C++ には const というキーワードがあります。さほど難しい概念でもないし、一度覚えてしまえば、逆に使わないことが気持ち悪くなるぐらい、不可欠なものです。料理でいえば、何かを切ったら包丁を拭くことと同じようなもので、プロにしてみれば、当たり前すぎて意識にも上らないような動作の一つでしょう。

ですが、私が今まで見てきたところでは、仕事で C や C++ を書いている人のうち、少なく見積もっても90%の人は、const を使いません。ですから、もしあなたが初心者だとして、このキーワードを使いこなせるようになれば、その時点で上位10%のプログラマーに食い込めることになります。さらに言うと、その使い方を覚えるのに、1時間もかかりません。

6つの意味

const キーワードは、一言でいうと、修飾する対象が不変であることを示すものです。ただし、その対象となるものは多岐にわたり、またそれによってニュアンスが微妙に異なってきます。

ここでは const キーワードの適用対象を「重要な順に」6つに大別し、その意味するところを解説していきます。

1. ポインタを修飾する

ポインタ変数は、アスタリスクより左側に const キーワードを置くと、それが読み取り専用ポインタであるという意味になります。たとえば次の p は、const ポインタです。

const char *p;

当然、その意図に反するコードはコンパイルエラーになります。ポインタを上のように修飾しておけば、間違って次のようなコードを書いても、コンパイラが見逃しません。

*p = '\0';  /* このコードはコンパイルが通らない */
p[15] = 0;  /* このコードはコンパイルが通らない */

このことが特に重要な意味を持つのは、ポインタを関数の引数にするときです。たとえば C の標準関数である memcpy の宣言は、次のようになっています。

void *memcpy(void *, const void *, size_t);

const ポインタを知っていれば、この関数は第1引数が書き込み先で、第2引数が読み取り元としか解釈しようがありません(ついでに言うと、戻り値は第1引数と同じだということも推測できます)。

このように、関数の引数に const ポインタを使うことは、関数を使う人に対する強力なメッセージになります。逆に言えば、あなたの関数が const「ではない」ポインタを不当に要求することは、あなたがだらしないプログラマーだと宣伝することと同じです。関数の設計には、十分な考慮を重ねましょう。

2. 参照を修飾する

参照は C++ で登場した概念です。その使用場面はほぼすべて、クラスのインスタンスを引数にするときであるといって差し支えありません。たとえば次のようにです。

TimeSpan subtract(const DateTime &lhs, const DateTime &rhs);

参照が出てきたら、それにはもう反射的に const を付けてしまいましょう。非 const の参照は、文法的には許されても、人道的には許されません。たとえば次のコードを見てください。

String s = "hello";

String t = toUpper(s);

このコードを見て、s の中身が変化してしまっているかもしれないなどと、誰が想像できるでしょうか。非 const の参照を使えば、そのような暴挙が許されてしまいます。非 const の参照を使いたくなる場面では、代わりに非 const のポインタを使いましょう。ポインタを受け取る関数であれば、おのずと次のような呼び出し方になるはずです。

String t = toUpper(&s);

そしてこのアスタリスクの存在が、プログラマーに警鐘を鳴らしてくれることになるのです。

3. 永続的な変数を修飾する

永続的な変数とは、グローバル変数、および static 修飾されたローカル変数のことを指します。これらに const キーワードをつけることは、単に代入ができないだけにとどまらず、少し特別な意味が加わります。それは、コンパイラに対して「ROM に置け」という指示になるということです。

このことを理解しないと、たとえば次のようなルックアップテーブルに RAM 領域を大量に奪われながら、その事実に気づかないということにもなりかねません。

double g_sin_table[4096] = {
  0.0000, 0.0004, 0.0008, 0.0012, 0.0015, 0.0019, 0.0023, 0.0027,
    /* 略 */
  0.9999, 0.9999, 0.9999, 0.9999, 0.9999, 0.9999, 1.0000, 1.0000,
};

次のように const を付けるだけで、このテーブルは ROM に配置され、RAM を消費しません。

const double g_sin_table[4096] = {
    /* 略 */
};

組み込みプログラマーは、テーブルといえば const が考えなしに飛び出るぐらいでないといけません。なぜなら、比較的規模の小さい組み込み製品では、今でも RAM が貴重であることが多いからです。

もう一つ、これに関連する注意点があります。永続的な変数がポインタの場合、const キーワードはアスタリスクの「右に」置かないと、ROM 配置の指示にはならないという点です。

IPortOutput *const g_nanaseg1[8] = {
  &g_port20, &g_port21, &g_port40, &g_port41,
  &g_port76, &g_port77, &g_port34, &g_port35,
};

このような const の使い方は見慣れないかもしれませんが、組み込みの世界では普通に出てきます。ぜひ、使いこなせるようになっておいてください。

4. メンバ変数を修飾する

クラスのメンバ変数を const 修飾できるということを知る人は少ないようです。それはおそらく、初期化子リストがあまり知られていないことに起因するのではないかと私は思っています。

ですから、初期化子リストを使えば次のように書けるものを――

class Real
{
private: const double m_val;
public: Real(double val) : m_val(val) { }
};

わざわざ次のように書いている人が多いのではないでしょうか。

class Real
{
private: double m_val;
public: Real(double val) { m_val = val; }
};

では、なぜこの違いが重要なのでしょうか。タイプする文字数が減るわけでもないのに。

よくあるたとえですが、人間をクラスに見立ててみます。

class Human
{
private:
  const char *m_name;  // 名前
  int m_age;           // 年齢
  double m_height;     // 身長
  BloodType m_blood;   // 血液型
};

メンバ変数をよくよく見ていくと、これらは生まれつき備わっている「性質」と、のちに変化していく「状態」に大別できることがわかります。これらのうち、「性質」に相当するメンバ変数はもれなく const 修飾すべきです。このように。

class Human
{
private:
  // 性質
  const char *const m_name;  // 名前
  const BloodType m_blood;   // 血液型
  // 状態
  int m_age;                 // 年齢
  double m_height;           // 身長
};

ここでの const の効果は2つあります。一つは「状態」を減らすこと。「状態」はバグの元凶ですから、少ないに越したことはありません。

もう一つは、コンストラクタで「性質」を与えざるを得なくなることです。const 修飾されたメンバ変数は、初期化子リストを使わなければ初期化できません。つまり、必然的にコンストラクタの引数となるわけです。たとえばこのように。

class Human
{
public:
  Human(const char *name, BloodType blood)
  : m_name(name)
  , m_blood(blood)
  {
  }
};

このコードが語るには、「人間を生み出したければ、二つの性質を与えよ」というわけです。このように、「性質」と「状態」がきちんと分けられたクラスでは、自分自身が何者かをコードが語り出すようになります。これこそまさに「オブジェクト指向」だと思いませんか。

5. メンバ関数を修飾する

メンバ関数を const 修飾すると、そのメンバ関数を呼び出してもインスタンスの状態は変化しませんよ、というサインになります。

class File
{
public:
  bool isEnd() const;
  char get() const;
};

その意味は、クラスを使う人に安心感を与えるというだけではありません。C++ には、const ポインタから非 const のメンバ関数を呼べないというルールがあります。たとえば次のようなコードは、呼び出されるメンバ関数がもれなく const 修飾されていないと、コンパイルが通りません。

const File *file = open("test.txt");  // const ポインタ

if (!file->isEnd())
{
  char c = file->get();
}

つまり、メンバ関数が適切に const 修飾されていないと、そのクラスを使う人に迷惑がかかる場合があるということです。

余談ですが、get で始まるメンバ関数は必ず const にしておくことをおすすめします。そうでないと、多くのプログラマーの予測を裏切ることになるからです。const にできない場合は、別の動詞を当てることを検討してください。また、戻り値が bool 型であるメンバ関数も、ほぼ例外なく const になるはずです。

6. 一時的な変数を修飾する

一時的な変数とは、引数、および static 修飾のないローカル変数を指します。これらを const 修飾した場合、「代入を許さない」以上の意味はありません。たとえば次のコードに登場する const がすべてそうです。

bool isClockwise(const uint32_t a, const uint32_t b)
{
  const uint32_t diff = b - a;
  return (diff & 0x80000000U) != 0;
}

白状すると、私はこの種の const だけは実践していません。事故防止に有効なのは認めますが、コードが病的にうるさくなってしまうからです。特に引数に const をつけると、関数は当然その型で宣言することになります。そのような、関数の内部事情を外部に宣言する行為が、私にはどうにも気に入りません。(const がデフォルトならいいのに!)

ですが、もちろんこれは私の好みの問題です。MISRA C のような厳しいコーディング規約だとこのような変数にも const を付けることを強いますし、それが堅牢なプログラムにつながることも事実です。この点は、状況を的確に判断して使い分けてください。