ムーブセマンティクス

代入すると、変数間で 所有権 が移動します。

fn main() {
    let s1: String = String::from("Hello!");
    let s2: String = s1;
    println!("s2: {s2}");
    // println!("s1: {s1}");
}
  • s1s2 に代入すると、所有権が移動します。
  • s1 がスコープ外になると、何も所有してないからです(何も所有しません)。
  • s2 がスコープ外になると、文字列データは解放されます。

s2 に移動する前:

StackHeaps1ptrHello!len6capacity6

s2 に移動した後:

StackHeaps1ptrHello!len6capacity6s2ptrlen6capacity6(inaccessible)

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

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

fn main() {
    let name = String::from("Alice");
    say_hello(name);
    // say_hello(name);
}
This slide should take about 5 minutes.
  • これは、std::move を使用しない限り(かつムーブ コンストラクタが定義されていない限り)値をコピーする、C++ のデフォルトとは逆であることを説明します。

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

  • 単純な値(整数など)には Copy のマークを付けることができます(後のスライドを参照)。

  • Rust では、クローンは明示的に clone を使用して行われます。

say_hello の例の内容は次のとおりです。

  • say_hello の最初の呼び出しで、mainname の所有権を放棄します。その後は main 内で name が使用できなくなります。
  • name に割り当てられたヒープメモリは、say_hello 関数の最後で解放されます。
  • mainname を参照として渡し(&name)、say_hello がパラメータとして参照を受け入れる場合、main は所有権を保持できます。
  • または、main が最初の呼び出しで name のクローン(name.clone())を渡すこともできます。
  • Rust では、ムーブ セマンティクスをデフォルトにし、クローンをプログラマに明示的に行わせています。これにより、C++ に比べて意図せずコピーを作成するリスクが低減されています。

その他

Defensive Copies in Modern 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++ の = は、コピーまたは移動される型によって決定される任意のコードを実行できます。