Open8

値渡しとか参照渡しとか共有渡しとか

eduidleduidl

記事書こうと思ったがまとまらなすぎて、スクラップに書き殴ることにした

eduidleduidl

まず関数呼び出しが本質じゃないと思っている.

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

先行例

「関数の引数の話だけでなく、代入の場合もまとめて同じ呼び方で呼んだらどうか」ということです。

この記事で見てきたように、引数の渡し方と代入のやり方にはハッキリした対応関係があります。

https://qiita.com/agajo/items/b21abdfb0baed340e897

この記事は JavaScript に完全な参照渡しなど存在しないのだから、わざわざ値渡し/参照渡しで分類せずとも、値渡し/参照渡しと言われてきた挙動を説明できるパターンの紹介です

https://qiita.com/yuta0801/items/f8690a6e129c594de5fb

eduidleduidl

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つしか持てないことになっている.さらに極端かもしれないが、可変参照が存在しない言語を考えることは不可能だろうか?

eduidleduidl

共有渡し的振る舞い

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の振る舞いに近くなる.

eduidleduidl

参照の値渡し的振る舞い

共有渡しと参照の値渡しは同じものを指しているのだが、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
}
eduidleduidl

考えれば考えるほど全てが値という気持ちになってくる.

実体があって、言語によって参照やポインタ、参照への参照だったり色々ある.で、全部まとめて値でいいんじゃないの?って.

力の統一理論ならぬ渡しの統一理論