読者です 読者をやめる 読者になる 読者になる

右上➚

プログラミングに関するメモをのこしていきます

値と参照について

「値」と「参照」という言葉があります。 このへんの言葉について、今の理解をまとめておこうと思います。 言葉の定義や理解が誤っている部分があればご指摘ください。

まず、前提として以下では、「値」ベースの言語として 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++ としていますが、pjohn のコピーであって john ではありませんから、inc_age から戻ってきて john.age を参照しても 20 のまま変化が無いはずです。

したがって結果として 20 が出力されます。

「参照」ベースの場合

参照ベースの言語の場合、john 変数のメモリ上での表現は、ヒープに置かれた Person { "john", 20 } というオブジェクトへのポインタになります。
inc_age にこれを引数として渡すと、ポインタの値がコピーされますから、pjohn は同じオブジェクトを参照しているポインタになります。
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

pPerson* 型であること、そして inc_agejohn のアドレスを渡していることに注目してください。 この場合、pjohn を参照するポインタですから、結果は 21 になります。

C++ の場合

C++ の場合、言語機能として「参照渡し」という機能があります。

void inc_age(Person& p) {
    p.age++;
}

Person john = Person { "john", 20 };
inc_age(john);
print(john.age); // => 21

pPerson& 型であること、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 と参照」とかについてまとめようと思ったのですが、前提として「値」「参照」についてまとめていないと書きにくいなと思ったのでまとめておきました。

内部表現を知ることでハマりやすい点の回避にもつながると思うので、この辺はきちんと理解しておきたいです。