ムーブセマンティクス

代入を行うと、変数間で_所有権_が移動します:

// 著作権 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0

fn main() {
    let s1 = String::from("Hello!");
    let s2 = s1;
    dbg!(s2);
    // dbg!(s1);
}
  • s1 から s2 への代入によって所有権が移動します。
  • s1 がスコープを抜けても、何も起こりません。何も所有していないためです。
  • s2 がスコープを抜けると、文字列データは解放されます。

s2 へムーブする前:

StackHeaps1ptrHello!len6capacity6

s2 へムーブした後:

StackHeaps1ptrHello!len6capacity6s2ptrlen6capacity6(inaccessible)

関数に値を渡すと、その値は関数 パラメータに代入されます。これにより所有権が移動します:

// 著作権 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0

fn say_hello(name: String) {
    println!("Hello {name}")
}

fn main() {
    let name = String::from("Alice");
    say_hello(name);
    // say_hello(name);
}
  • これは C++ のデフォルトとは逆であることにも触れてください。C++ では std::move を使わない限り値渡しでコピーされます(そしてムーブコンストラクタが定義されている場合です!)。

  • 移動するのは所有権だけです。データそのものを操作するためのマシンコードが 生成されるかどうかは最適化の問題であり、そのようなコピーは 積極的に最適化で取り除かれます。

  • 単純な値(整数など)には Copy を付けられます(後のスライドを参照)。

  • Rust では、クローンは明示的です(clone を使います)。

say_hello の例では:

  • 最初の say_hello 呼び出しで、mainname の所有権を手放します。 その後、main の中では name をもう使えません。
  • name のために確保されたヒープメモリは、say_hello 関数の終わりで 解放されます。
  • main は、name を参照(&name)として渡し、かつ say_hello が パラメータとして参照を受け取るなら、所有権を保持できます。
  • あるいは、main は最初の呼び出しで name のクローン (name.clone())を渡すこともできます。
  • Rust は、ムーブセマンティクスをデフォルトにし、プログラマにクローンを 明示させることで、C++ よりも意図せずコピーを作りにくくしています。

さらに詳しく

現代の C++ における防御的コピー

現代の C++ では、これは別の方法で解決します:

std::string s1 = "Cpp";
std::string s2 = s1;  // s1 のデータを複製する。
  • s1 のヒープデータは複製され、s2 はそれ自身の独立したコピーを持ちます。
  • s1s2 がスコープを抜けると、それぞれが自分のメモリを解放します。

コピー代入の前:

StackHeaps1ptrCpplen3capacity3

コピー代入の後:

StackHeaps1ptrCpplen3capacity3s2ptrCpplen3capacity3

要点:

  • C++ は Rust とは少し異なる選択をしています。= はデータをコピーするため、 文字列データはクローンされなければなりません。そうでないと、どちらかの 文字列がスコープを抜けたときに二重解放が起きてしまいます。

  • C++ には std::move もあり、これは値からムーブしてよいことを示すために 使われます。例が s2 = std::move(s1) だった場合、ヒープ割り当ては 発生しません。ムーブ後の s1 は、有効ではあるものの未規定の状態に なります。Rust とは異なり、プログラマは s1 を引き続き使用できます。

  • Rust と異なり、C++ の = は、コピーまたはムーブされる型によって決まる 任意のコードを実行できます。