引数に込めた意味

ユキ先輩。if文for文の使い方について、いろいろと教えてもらったところなんですけど……
うん。どうだった?
いやぁ。基本だからもう大丈夫って思ってたところも、意外に奥が深いんだなぁって感じました。
ふふふ。そこを分かってもらえて嬉しいわ。
えへへ。そういうの、もっと知りたいです!
それじゃあ、次は引数の話をしましょうか。
やったー!お願いしまーす。

次のような、ただ足し算をするだけの関数があったとします。

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;
}

上記の例では、関数の途中で引数LHSRHSの値を表示させています。このとき、引数の名前から、それぞれ足し算の左側と右側にくる値だと考えているわけです。ところが実際には、ここより上のほうでLHSの値が変わってしまっています。

名前は、プログラミングをする際の重要な手がかりです。わざわざ具体的な名前を付けておいて、そこに違うものを割り当てるのは、必要以上にプログラムを複雑にする行為だといえるでしょう。

つまり、引数に別の値を代入するとミスリードになっちゃうんですね。
そういうこと!
ここがポイント!
引数には意味が込められています。意味と中身が矛盾するのを避けるために、引数には別の値を代入しないようにしましょう。

代入を必要最小限にするためのコツ

ここで、名前を付け直す前の例に戻りましょう。

int Add(int a, int b) {
  a += b; /* ← 意味を無視して代入 */
  return a;
}

これらの引数(ab)は、意味を簡単にイメージできるような名前をもっていません。でも、たとえ名前が曖昧でも、よく見れば意味が隠れているものです。そのため、やはり引数への代入はおすすめしません。

代わりに、次のようにすればいいでしょう。

int Add(int a, int b) {
  int result = a + b;
  return result;
}
計算結果を新しい変数に割り当てているのが分かるかしら?
はい。こうすれば名前と値が矛盾することはないんですね!
考え方としてはね、頭の中でconstを付けてみるといいわよ。
え?それってどういうことですか?

プログラム中に現れる変数(引数を含む)のうち、初期化したあとで値を変える必要があるものは、それほど多くはありません。そこで、「実はほとんどの変数が不変なのだ」とイメージしてみましょう。C言語では、「不変(constant)」を意味するconstを付けた変数は、値の変更ができなくなります。

例えば、次のようにabconstを付けたと考えると……

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が「値渡し」になっているためです。「値渡し」の引数は関数呼び出しの際に作られる一時的な変数なので、たとえ関数の内部で値が変更されたとしても、関数を呼び出す側には影響がありません。つまり、nconstが付いているかどうかは、この関数を呼び出す側の関心事ではないということです。

ここで、関数は作る回数よりも、使う回数の方が多いことを思い出しましょう。関数を使うときのことを中心に考えるなら、第3引数のnconstを付けてもメリットがありません。また、nconstを付けないほうが、第2引数のsrcに付いている大事なconstが際立ちます。

これをふまえて、最初に出てきた足し算をする関数の引数にはconstが要るかしら?
えっと。abはどっちも「値渡し」になってるから……
int Add(int a, int b) {
  return a + b;
}
呼び出し側にメリットがないから、constは要らないです!
そのとおり!でも関数を作るときは、頭の中でconstを付けて「不変」だと思うのがコツなのよ。
引数に別の値を代入しないように、気を付けやすくなるってわけですね。
ここがポイント!
プログラム中に現れる変数(引数も含む)は、実はほとんどが不変!そう考えれば、名前と中身の矛盾を避けやすくなります。