値渡しとか参照渡しとか共有渡しとか
記事書こうと思ったがまとまらなすぎて、スクラップに書き殴ることにした
〇〇渡しって色々あるけど本質的な違いがわからない.
- 値渡し
- 参照渡し
- 共有渡し・参照の渡し・オブジェクト渡し、etc...
まず関数呼び出しが本質じゃないと思っている.
class A:
def __init__(self, value: int) -> None:
self.value = value
def func(a: A):
a.value = 10
a = A(100)
a = A(1)
func(a)
print(a.value) #=> 10
上のような例をもって、参照渡しでないと主張されるが、下の例との本質的差異がわからない.
a = A(1)
# func(a) start
a1 = a
a1.value = 10
a1 = A(100)
# func(a) end
print(a.value) #=> 10
先行例
「関数の引数の話だけでなく、代入の場合もまとめて同じ呼び方で呼んだらどうか」ということです。
この記事で見てきたように、引数の渡し方と代入のやり方にはハッキリした対応関係があります。
この記事は JavaScript に完全な参照渡しなど存在しないのだから、わざわざ値渡し/参照渡しで分類せずとも、値渡し/参照渡しと言われてきた挙動を説明できるパターンの紹介です
C++ではコンストラクタと代入演算子で別物として存在する.
そして、以下のように参照を使ってa_ref = A{100}
とできるのは、代入演算子があるから.逆にPython等でできないのはこういう形の代入演算ができないから.
#include <iostream>
struct A {
explicit A(int value) : value_(value) {}
~A() = default;
A(const A& other) : value_(other.value_) {
std::cout << "copy constructor" << std::endl;
}
const A& operator=(const A& other) {
std::cout << "copy assignment" << std::endl;
value_ = other.value_;
return *this;
}
A(A&& other) = delete;
A& operator=(A&& other) noexcept {
std::cout << "move assignment" << std::endl;
if (this != &other) {
value_ = other.value_;
}
return *this;
}
int value_;
};
int main() {
{
std::cout << "[value]" << std::endl; // [value]
A a{1}; //
auto a_copy = a; // copy constructor
a_copy.value_ = 10; //
a_copy = A{100}; // move assignment
std::cout << a.value_ << std::endl; // 1
}
{
std::cout << "[reference]" << std::endl; // [reference]
A a{1}; //
auto& a_ref = a; //
a_ref.value_ = 10; //
a_ref = A{100}; // move assignment
std::cout << a.value_ << std::endl; // 100
}
}
ところで代入演算が行えることは参照の必須要件だろうか?だとしたらconst 参照は?
例えば安全でおなじみのRustでは可変参照は同時に1つしか持てないことになっている.さらに極端かもしれないが、可変参照が存在しない言語を考えることは不可能だろうか?
共有渡し的振る舞い
std::shared_ptr<T>
を使って再現してみる.
#include <iostream>
#include <memory>
class A {
// 省略
};
int main() {
{
std::cout << "[shared pointer]" << std::endl; // [shared pointer]
auto a = std::make_shared<A>(1); //
auto a_copy = a; //
a_copy->value_ = 10; //
*a_copy = A(100); // move assignment
a_copy = std::make_shared<A>(1000); //
std::cout << a->value_ << std::endl; // 100
}
}
[shared pointer]
move assignment
100
C++ではできるムーブ代入を取っ払えば、そのままPythonの振る舞いに近くなる.
参照の値渡し的振る舞い
共有渡しと参照の値渡しは同じものを指しているのだが、std::reference_wrapper<T>
を使うと用語の気持ちがわかる気がする.
#include <functional>
#include <iostream>
class A {
// 省略
};
static A g_a{1000};
int main() {
{
std::cout << "[reference wrapper]" << std::endl; // [reference wrapper]
A origin{1}; //
auto a = std::ref(origin); //
auto a_copy = a; //
a_copy.get().value_ = 10; //
a_copy.get() = A(100); // move assignment
a_copy = std::ref(g_a); //
std::cout << a.get().value_ << std::endl; // 100
}
}
[reference wrapper]
move assignment
100
ところで、C++の参照(std::reference_wrapper
ではない)は参照先を変更することはできないが、Rustでは以下のようにできる.
fn main() {
let mut a = 10;
let mut b = 100;
let mut ref_ = &mut a;
ref_ = &mut b;
*ref_ = 1000;
println!("{}", b); //=> 1000
}
考えれば考えるほど全てが値という気持ちになってくる.
実体があって、言語によって参照やポインタ、参照への参照だったり色々ある.で、全部まとめて値でいいんじゃないの?って.
力の統一理論ならぬ渡しの統一理論
javascript だと arguments が 仮引数 と エイリアスの関係(いわゆる 代入が反映される 参照渡し的動作)ですね。(arguments が特殊なオブジェクトなので 唯一の動作ではありますが