値と参照について
「値」と「参照」という言葉があります。 このへんの言葉について、今の理解をまとめておこうと思います。 言葉の定義や理解が誤っている部分があればご指摘ください。
まず、前提として以下では、「値」ベースの言語として C, C++, Rust などを、「参照」ベースの言語として Java, C#, Ruby などを想定しています。 (もちろん言語によってはハイブリッドなものもあります: Crystal, Go, D, ...)
そもそも「値」「参照」とは
「値」は「実体」、「参照」は「実体へのポインタ」です。
int
の配列みたいなものを考えてみます。
[1, 2, 3]
をメモリ上にどう表現できるでしょうか。
配列ですから、単純にメモリ上のどこかに以下のような領域を作ればよさそうです。
+---+ | 1 | +---+ | 2 | +---+ | 3 | +---+
これが「値」であり配列の実体です。 そして、実体の配置されたメモリ領域へのポインタが「参照」です。
スタック上の表現
さて、プログラム上ではこの配列のようなオブジェクトを、ローカル変数としてスタック上で表現したり、関数に引数として渡したりします。 では、スタック上での配列の表現はどうなっているのか考えてみます。
ここで、「値」と「参照」という言葉が重要になります。
「値」ベースな言語では、スタック上に
+---+ | 1 | +---+ | 2 | +---+ | 3 | +---+
をそのままべたっと配置します。
一方で、「参照」ベースな言語では、ヒープ上に
+---+ | 1 | +---+ | 2 | +---+ | 3 | +---+
を配置し、スタック上では実体へのポインタという表現になります。 (必ずしもヒープに置くとは限らない?処理系や最適化によってはスタックに置くこともありえる?とにかくローカル変数などの表現としては実体へのポインタという形をとるということ)
値渡し・参照渡し
値渡しとか参照渡しという言葉があります。
以下に擬似コードを一つ書いてみます。(C 風に書いていますが C ではないと思ってください)
void inc_age(Person p) { p.age++; } Person john = Person { "john", 20 }; inc_age(john); print(john.age); // => ??
このコード、処理系が「値」ベースか「参照」ベースかで結果が異なります。
「値」ベースの場合
値ベースの言語の場合、inc_age
の引数に john
を渡した時には、inc_age
内のローカル変数(引数) p
のために、john
のコピーが作られます。
inc_age
内で p.age++
としていますが、p
は john
のコピーであって john
ではありませんから、inc_age
から戻ってきて john.age
を参照しても 20 のまま変化が無いはずです。
したがって結果として 20
が出力されます。
「参照」ベースの場合
参照ベースの言語の場合、john
変数のメモリ上での表現は、ヒープに置かれた Person { "john", 20 }
というオブジェクトへのポインタになります。
inc_age
にこれを引数として渡すと、ポインタの値がコピーされますから、p
と john
は同じオブジェクトを参照しているポインタになります。
p.age++
とすると p
が参照するオブジェクトが変更されます。これは john
が参照するオブジェクトと同一ですから、john.age
も 21 に変化します。
したがって結果として 21
が出力されます。
C の場合
C の場合、ポインタが直接表現できますから、参照渡しの挙動を模倣することができます。
void inc_age(Person *p) { p->age++; } Person john = Person { "john", 20 }; inc_age(&john); print(john.age); // => 21
p
が Person*
型であること、そして inc_age
に john
のアドレスを渡していることに注目してください。
この場合、p
は john
を参照するポインタですから、結果は 21
になります。
C++ の場合
C++ の場合、言語機能として「参照渡し」という機能があります。
void inc_age(Person& p) { p.age++; } Person john = Person { "john", 20 }; inc_age(john); print(john.age); // => 21
p
が Person&
型であること、inc_age
には john
をそのまま渡しているように見えることに注目してください。
これは C++ の提供する機能で、コンパイルすると、Person&
は実質 Person*
と同じ表現になります。
p->age
ではなく p.age
と書けること、&john
ではなく john
のままで参照渡しが実現できるようになっています。
単純なポインタを使っても同じことが出来ますが、ポインタと違って nullptr
になることがないという特徴があります。
参照のハマりやすい点
個人的に参照ベースの言語でハマりやすいなと感じるのは以下のようなコードです。
void some_function(Person p) { p.age++; p = new Person("bob", 30); } Person p = new Person("john", 20); some_function(p); println(p.name); // => john println(p.age); // => 21
p.age++
の部分は呼び出し元のオブジェクトに反映されるのに、p = new Person(...)
の部分はなんで反映されないの!ってなります。(なりません?)
本質的にポインタの値渡しにすぎないんだということを理解していればまぁ納得なのですが...
(ちなみに C++ や D の参照渡しだと p = new Person(...)
的なコードも呼び出し元に反映されます。)
値のハマりやすい点
ハマりやすいというか、気がつかないままパフォーマンスが悪くなりやすいのが値ベースの言語の弱点でしょう。
void print_object(HugeObject obj) { printf("%s\n", obj.name); } HugeObject obj = ...; print_object(obj);
このようなコードを書くと、ただ名前を表示するだけの関数が激重になる可能性があります。 値ベースの言語では、引数として値を渡すとまるっとそのコピーをつくりますから、不要にもかかわらず巨大な値のコピーを作ってしまいます。
まとめ
「ムーブセマンティクス」とか「immutable と参照」とかについてまとめようと思ったのですが、前提として「値」「参照」についてまとめていないと書きにくいなと思ったのでまとめておきました。
内部表現を知ることでハマりやすい点の回避にもつながると思うので、この辺はきちんと理解しておきたいです。