次のような、ただ足し算をするだけの関数があったとします。
int Add(int a, int b) {
return a + b;
}
「こんな関数は要らないでしょ!」というツッコミが聞こえてきそうですが……話を単純にするための例だと思ってください。
さて、この関数の実装が、もしも次のようになっていたとしたらどうでしょうか。
int Add(int a, int b) {
a += b;
return a;
}
足し算の結果をいったん引数a
に格納してから、それを返しています。関数の内部的な実装の話なので、こうした実装の違いが呼び出し側に影響を与えることはありません。つまり、最初の例も2つ目の例も、同様に足し算を実行できる関数です。
引数への代入を避ける
識別子(この場合は引数の名前)を次のように変えれば、違和感の理由が浮き彫りになるでしょう。
int Add(int LHS, int RHS) {
LHS += RHS;
return LHS;
}
命名法のところでも紹介しましたが、LHSは「Left Hand Side(左側)」の略、RHSは「Right Hand Side(右側)」の略です。LHS + RHS
のように、足し算記号の左側・右側に置かれる値を意味する名前ですね。
このように、識別子に具体的な名前を付けてあげると、それぞれの意味が明確になります。そして、どの識別子にも意味があるのだとしたら、それを無視した値を割り当てると矛盾が生じてしまうはずです。
LHS
に、足し算の結果を割り当ててしまっているわね。
この例ほど小さな関数であれば、実際のところは大した問題ではないかもしれません。でも、もっと行数が増えていくと、そうも言っていられなくなります。
int Add(int LHS, int RHS) {
LHS += RHS; /* ← 名前の意味を無視して代入 */
……
printf("LHS = %d\n", LHS); /* ← まさか名前と違う値が入っているとは! */
printf("RHS = %d\n", RHS);
……
return LHS;
}
上記の例では、関数の途中で引数LHS
とRHS
の値を表示させています。このとき、引数の名前から、それぞれ足し算の左側と右側にくる値だと考えているわけです。ところが実際には、ここより上のほうでLHS
の値が変わってしまっています。
名前は、プログラミングをする際の重要な手がかりです。わざわざ具体的な名前を付けておいて、そこに違うものを割り当てるのは、必要以上にプログラムを複雑にする行為だといえるでしょう。
代入を必要最小限にするためのコツ
ここで、名前を付け直す前の例に戻りましょう。
int Add(int a, int b) {
a += b; /* ← 意味を無視して代入 */
return a;
}
これらの引数(a
とb
)は、意味を簡単にイメージできるような名前をもっていません。でも、たとえ名前が曖昧でも、よく見れば意味が隠れているものです。そのため、やはり引数への代入はおすすめしません。
代わりに、次のようにすればいいでしょう。
int Add(int a, int b) {
int result = a + b;
return result;
}
const
を付けてみるといいわよ。
プログラム中に現れる変数(引数を含む)のうち、初期化したあとで値を変える必要があるものは、それほど多くはありません。そこで、「実はほとんどの変数が不変なのだ」とイメージしてみましょう。C言語では、「不変(constant)」を意味するconst
を付けた変数は、値の変更ができなくなります。
例えば、次のようにa
とb
にconst
を付けたと考えると……
int Add(const int a, const int b) {
a += b; /* ← エラー */
return a;
}
引数に代入しようとしている行でコンパイルエラーが発生します。
一方、計算結果を新しい変数に代入する場合はエラーにはなりません(計算結果のresult
も不変なのでconst
が付いています)。
int Add(const int a, const int b) {
const int result = a + b;
return result;
}
const
を付けるのは「頭の中で」なんですか?実際のプログラムでも、どんどん付ければいいと思ったんですけど……
const
の数が多すぎて目がチカチカしてくるんじゃないかしら。
const
になってしまったら、ちょっと読みづらそうですね。
const
は意味があるところにだけ付けるのがおすすめよ。
実際のプログラム中でconst
を使いすぎると、どれが本当に大事なconst
なのかが分からなくなってしまいます。const
を付けるのは、メリットを感じられる場所に限定するのがいいでしょう。その際には、標準関数がお手本になります。
例えば、文字列をコピーするstrncpy()
は、次のような形をしています。
char *strncpy(char *dest, const char *src, size_t n);
第1引数のdest
は、「ポインタ渡し」になっています。これは、関数を呼び出す側にあるバッファに、文字列のコピーを書き戻してもらうためのアドレスです。値を書き込む必要があるので、const
は付いていません。
第2引数のsrc
も「ポインタ渡し」ですが、こんどはconst
が付いています。こちらはコピーする文字列を関数に渡すためのものであり、このアドレスには何も書き込んでほしくはありません。const
があれば「この関数はsrc
の文字列を変更することはないんだな」と分かるので、意図を理解して使えます。
第3引数のn
は、コピーする文字数です。この値も変更してほしいものではありませんが、const
がありませんね。これは、引数n
が「値渡し」になっているためです。「値渡し」の引数は関数呼び出しの際に作られる一時的な変数なので、たとえ関数の内部で値が変更されたとしても、関数を呼び出す側には影響がありません。つまり、n
にconst
が付いているかどうかは、この関数を呼び出す側の関心事ではないということです。
ここで、関数は作る回数よりも、使う回数の方が多いことを思い出しましょう。関数を使うときのことを中心に考えるなら、第3引数のn
にconst
を付けてもメリットがありません。また、n
にconst
を付けないほうが、第2引数のsrc
に付いている大事なconst
が際立ちます。
const
が要るかしら?
a
とb
はどっちも「値渡し」になってるから……
int Add(int a, int b) {
return a + b;
}
const
は要らないです!
const
を付けて「不変」だと思うのがコツなのよ。