コンテンツへスキップ
Topos Uranos

要約

このレッスンでは、データがどのような経路で関数に入り、その結果がどのような経路で出てくるのかを学ぶ。既定では、C++ は実引数を値で渡す。仮引数はコピーであり、関数がそれに何をしても原本には届かない。これに対して参照は、すでに存在するメモリの区画の第二の名前であり、関数が呼び出し側の変数を変更することを可能にする。const 参照は、大きなデータをコピーせずに、しかも変更できない形で読むことを可能にする。複数の結果を返すために、ライブラリは std::pair と std::tuple を用意しており、これらは構造化束縛で受け取る。続いて、名前のスコープ(どこで見えるか)と変数の生存期間(いつ存在するか)を、名前を隠す宣言や static ローカル変数とともに扱い、最後に、自分の変数より長く生き残る参照を取り上げる。これらすべてをまとめる概念は、プログラムの部分どうしのあいだのデータの流れである。すなわち、コピーか共有かという対比と、隔離の仕組みとしての名前の可視性である。

学習目標

  1. 実引数が値で渡されるか参照で渡されるかに応じて、関数が実引数に及ぼす作用を予測する。
  2. 各仮引数の渡し方(小さな型は値、大きなデータは const 参照、変更するものは参照)を選び、その選択を正当化する。
  3. std::pair または std::tuple で複数の結果を返し、構造化束縛で受け取る。
  4. プログラムの各変数のスコープと生存期間を、static ローカル変数や別の変数に隠される変数も含めて決定する。
  5. もう存在しないローカル変数や一時的な値への参照を、それを知らせるコンパイラの警告とともに見抜き、修正する。

値渡し

仮引数はコピーである

レッスン 関数 では、呼び出しのたびに、関数の仮引数とローカル変数を収めたフレームがスタック上に作られ、戻るときにそのフレームが消えることを見た。残っているのは、仮引数とそれを初期化する実引数とがどういう関係にあるのかを明らかにすることである。C++ の既定の答えは最も単純なものである。仮引数は新しいローカル変数であり、実引数の値のコピーで初期化される。そうだとすれば、自分の仮引数を変更する関数はそのコピーを変更しているのであって、呼び出し側の変数は変わらない。以下のプログラムは、二つの整数を交換しようとする関数でこれを確かめる。

C++ のプログラム
#include <print>​// a と b の交換を試みる。交換されるのはそのコピーだけである。void swapValues(int a, int b){    const int saved{a};    a = b;    b = saved;    std::println("呼び出し中:a = {}, b = {}", a, b);}​int main(){    int x{1};    int y{2};    swapValues(x, y);    std::println("呼び出し後:x = {}, y = {}", x, y);}
出力
呼び出し中:a = 2, b = 1呼び出し後:x = 1, y = 2

関数の中では交換は正しく行われるが、呼び出しのあと x と y は元の値を保っている。コンパイラは警告しない。警告すべきことが何もないからである。このコードは、終わると消える二つのローカル変数を交換しているにすぎない。図は、呼び出しのあいだのスタックのフレームを追う。

例二つのフレーム、四つの区画

main のフレームには x と y がある。呼び出しはその上に、自分の区画をもつ swapValues のフレームを作る。

証明

  1. 呼び出しの前、main のフレームには二つの区画がある。1 の入った x と、2 の入った y である。

  2. 呼び出しによって swapValues のフレームが作られる。その仮引数 a と b は新しい区画であり、そこに実引数の値 1 と 2 がコピーされる。この瞬間から、a と x のあいだには、同じ値から始まったということ以外、何のつながりも残らない。

  3. ローカル変数 saved は同じフレームの中に作られ、a のコピーを保持する。

  4. 二つの代入が a と b を交換する。変化する区画はすべて上のフレームにあり、main の区画には手が触れられない。

  5. 戻るとき、swapValues のフレームは、その中で行われた交換もろとも丸ごと消える。x と y は相変わらず 1 と 2 である。

コピーが守るもの

値渡しは欠陥ではなく、関数を扱いやすいものにする規則である。第一に、実引数は変数にかぎらず任意の式でよい。swapValues(x + 1, 7) は正しい。仮引数は、どこから来たものであれ一つの値で初期化されるからである。第二に、呼び出し側は本体を読まなくても、宣言だけから自分の変数が変わらないことを知る。第三に、関数の中では仮引数はローカル変数の一つにすぎず、外に影響を及ぼさずに作業用の変数として使える。

C++ のプログラム
#include <print>​// n の十進法での桁数。// 事前条件:n >= 0。// 事後条件:n の桁数を返す(0 なら 1)。int digitCount(int n){    int count{1};    while (n >= 10) {        n = n / 10;        ++count;    }    return count;}​int main(){    const int population{48'210};    std::println("{} は {} 桁", population, digitCount(population));}
出力
48210 は 5 桁

この関数は、仮引数を 10 で割り続けて一桁にすることで仮引数を壊してしまう。だがそれはコピーなので、population は無傷のままであり、const にしてよい。ここから、コース全体を通じて使える読み方の規則が導かれる。仮引数に、このあとの節で学ぶ記号 & が付いていなければ、関数がそれに何をしようと、呼び出し側の変数を変更することはできない。