web-dev-qa-db-ja.com

Rustの移動セマンティクスとは何ですか?

Rustでは、参照する2つの可能性があります

  1. Borrow、つまり、参照を取得しますが、参照先の変更は許可しません。 _&_演算子は、値から所有権を借用します。

  2. 可変的に借りる、つまり、参照を取得して宛先を変更します。 _&mut_演算子は、値から所有権を可変的に借用します。

借用ルールに関するRustドキュメント は次のように述べています。

まず、借用は所有者の範囲を超えない範囲で継続する必要があります。次に、これら2種類の借用のいずれかを使用できますが、同時に両方を使用することはできません。

  • リソースへの1つ以上の参照(_&T_)、
  • 正確に1つの可変参照(_&mut T_)。

参照を取るということは、値へのポインターを作成し、そのポインターによって値にアクセスすることだと思います。より単純な同等の実装があれば、これはコンパイラによって最適化される可能性があります。

ただし、moveの意味と、その実装方法がわかりません。

Copyトレイトを実装するタイプの場合、それはコピーを意味します。ソースからメンバーごとに構造体を割り当てるか、memcpy()を割り当てます。小さな構造体またはプリミティブの場合、このコピーは効率的です。

そしてmoveの場合?

この質問は 移動セマンティクスとは何ですか? の重複ではありません。RustとC++は異なる言語であり、移動セマンティクスは2つで異なるためです。

21
nalply

セマンティクス

Rustは、 アフィン型システム として知られているものを実装します。

アフィン型は、アフィン論理に対応する、より弱い制約を課す線形型のバージョンです。 アフィンリソースは1回しか使用できませんが、線形リソースは1回使用する必要があります。

Copyではないために移動されるタイプは、アフィンタイプです。一度使用することも、まったく使用しないこともできます。

Rustは、これを所有権中心の世界観(*)で所有権の移転と見なします。

(*)Rustに取り組んでいる人々の中には、私がCSにいるよりもはるかに資格があり、彼らは故意にアフィン型システムを実装しました;しかし、数学を公開するHaskellとは対照的です-y/cs -yの概念、Rustは、より実用的な概念を公開する傾向があります。

注:_#[must_use]_でタグ付けされた関数から返されるアフィン型は、実際には私の読書からの線形型であると主張することができます。


実装

場合によります。 Rustは速度を重視して構築された言語であり、使用されるコンパイラに応じて、ここで多数の最適化パスが実行されることに注意してください。 (rustc + LLVM、この場合)。

関数本体内( playground ):

_fn main() {
    let s = "Hello, World!".to_string();
    let t = s;
    println!("{}", t);
}
_

(デバッグで)LLVM IRをチェックすると、次のように表示されます。

_%_5 = alloca %"alloc::string::String", align 8
%t = alloca %"alloc::string::String", align 8
%s = alloca %"alloc::string::String", align 8

%0 = bitcast %"alloc::string::String"* %s to i8*
%1 = bitcast %"alloc::string::String"* %_5 to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %1, i8* %0, i64 24, i32 8, i1 false)
%2 = bitcast %"alloc::string::String"* %_5 to i8*
%3 = bitcast %"alloc::string::String"* %t to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %3, i8* %2, i64 24, i32 8, i1 false)
_

カバーの下で、rustcは"Hello, World!".to_string()の結果からmemcpysを呼び出し、次にtに呼び出します。非効率に見えるかもしれませんが、リリースモードで同じIRをチェックすると、LLVMがコピーを完全に排除したことがわかります(sが未使用であることがわかります)。

関数を呼び出すときにも同じ状況が発生します。理論的にはオブジェクトを関数スタックフレームに「移動」しますが、実際には、オブジェクトが大きい場合、rustcコンパイラは代わりにポインタを渡すように切り替える場合があります。

別の状況は、関数からreturningですが、それでもコンパイラは「戻り値の最適化」を適用し、呼び出し元のスタックフレームに直接ビルドする場合があります。つまり、呼び出し元はポインタを渡します。中間ストレージなしで使用される戻り値を書き込む場所。

Rustの所有権/借用の制約により、C++では到達が困難な最適化が可能になります(RVOもありますが、多くの場合適用できません)。

したがって、ダイジェストバージョン:

  • 大きなオブジェクトの移動は非効率的ですが、移動を完全に排除する可能性のある最適化がいくつかあります。
  • 移動にはstd::mem::size_of::<T>()バイトのmemcpyが含まれるため、大きなStringの移動は、保持している割り当て済みバッファーのサイズに関係なく数バイトしかコピーしないため、効率的です。
22
Matthieu M.

移動アイテムの場合、あなたは所有権の譲渡そのアイテムの所有権です。これがRustの重要な要素です。

構造体があり、ある変数から別の変数に構造体を割り当てたとします。デフォルトでは、これは移転であり、私は所有権を譲渡しました。コンパイラはこの所有権の変更を追跡し、古い変数を使用できなくなります。

pub struct Foo {
    value: u8,
}

fn main() {
    let foo = Foo { value: 42 };
    let bar = foo;

    println!("{}", foo.value); // error: use of moved value: `foo.value`
    println!("{}", bar.value);
}

それがどのように実装されるか。

概念的には、何かを移動しても、何もする必要はありません必要。上記の例では、実際にどこかにスペースを割り当ててから、別の変数に割り当てるときに割り当てられたデータを移動する理由はありません。私は実際にコンパイラが何をするのかわかりません、そしてそれはおそらく最適化のレベルに基づいて変化します。

ただし、実用的な目的では、何かを移動すると、そのアイテムを表すビットがmemcpyを介して複製されると考えることができます。これは、変数を関数に渡したときに何が起こるかを説明するのに役立ちますconsumes it、または関数から値を返すとき(繰り返しますが、オプティマイザーはそれを効率的にするために他のことを行うことができます、これはただです概念的に):

// Ownership is transferred from the caller to the callee
fn do_something_with_foo(foo: Foo) {} 

// Ownership is transferred from the callee to the caller
fn make_a_foo() -> Foo { Foo { value: 42 } } 

「でも待ってください!」とあなたは言います、「memcpyCopy!を実装する型でのみ機能します」。これはほとんど真実ですが、大きな違いは、型がCopyを実装する場合、sourcedestinationの両方がコピー後に使用できることです。

移動セマンティクスの考え方の1つは、コピーセマンティクスと同じですが、移動元のオブジェクトが使用できる有効なアイテムではなくなるという制限が追加されています。

ただし、多くの場合、別の方法で考える方が簡単です。実行できる最も基本的なことは、所有権を移動/譲渡することであり、何かをコピーする機能は追加の特権です。これがRustがモデル化する方法です。

これは私にとって難しい質問です! Rustをしばらく使用した後、移動のセマンティクスは自然です。省略した部分や説明が不十分な部分を教えてください。

12
Shepmaster

私自身の質問に答えさせてください。問題が発生しましたが、ここで質問することで、 ラバーダック問題解決 を実行しました。今分かります:

moveは、値の所有権の譲渡です。

たとえば、割り当て_let x = a;_は所有権を譲渡します。最初はaが値を所有していました。 letの後、値を所有するのはxです。 Rust以降はaを使用できません。

実際、letの後にprintln!("a: {:?}", a);を実行すると、Rustコンパイラは次のように言います:

_error: use of moved value: `a`
println!("a: {:?}", a);
                    ^
_

完全な例:

_#[derive(Debug)]
struct Example { member: i32 }

fn main() {
    let a = Example { member: 42 }; // A struct is moved
    let x = a;
    println!("a: {:?}", a);
    println!("x: {:?}", x);
}
_

そして、これmoveはどういう意味ですか?

コンセプトはC++ 11から来ているようです。 C++移動セマンティクスに関するドキュメント は次のように述べています。

クライアントコードの観点から、コピーの代わりに移動を選択することは、ソースの状態がどうなるかを気にしないことを意味します。

あは。 C++ 11は、ソー​​スで何が起こるかを気にしません。したがって、この意味で、Rustは、移動後にソースの使用を禁止することを自由に決定できます。

そしてそれはどのように実装されていますか?

知りません。しかし、Rustは文字通り何もしません。xは同じ値の単なる別の名前です。名前は通常コンパイルされます(もちろんデバッグシンボルを除く)。したがって、バインディングの名前がaまたはxのどちらであっても、同じマシンコード。

C++はコピーコンストラクターの省略でも同じことをするようです。

何もしないことが最も効率的です。

1
nalply

関数に値を渡すと、所有権も譲渡されます。他の例と非常によく似ています。

struct Example { member: i32 }

fn take(ex: Example) {
    // 2) Now ex is pointing to the data a was pointing to in main
    println!("a.member: {}", ex.member) 
    // 3) When ex goes of of scope so as the access to the data it 
    // was pointing to. So Rust frees that memory.
}

fn main() {
    let a = Example { member: 42 }; 
    take(a); // 1) The ownership is transfered to the function take
             // 4) We can no longer use a to access the data it pointed to

    println!("a.member: {}", a.member);
}

したがって、予想されるエラー:

post_test_7.rs:12:30: 12:38 error: use of moved value: `a.member`
1
Akavall