web-dev-qa-db-ja.com

Rustは移動セマンティクスをどのように提供しますか?

Rust言語のWebサイト は、言語の機能の1つとしてセマンティクスを移動すると主張しています。しかし、移動のセマンティクスがRustでどのように実装されているかはわかりません。

錆箱は、移動セマンティクスが使用される唯一の場所です。

let x = Box::new(5);
let y: Box<i32> = x; // x is 'moved'

上記のRustコードは、次のようにC++で記述できます。

auto x = std::make_unique<int>();
auto y = std::move(x); // Note the explicit move

私の知る限り(間違っている場合は訂正してください)、

  • Rustにはコンストラクタがありません。コンストラクタを移動することは言うまでもありません。
  • 右辺値参照はサポートされていません。
  • Rvalueパラメータを使用して関数オーバーロードを作成する方法はありません。

Rustは移動セマンティクスをどのように提供しますか?

42
user3335

C++から来るとき、それは非常に一般的な問題だと思います。 C++では、コピーと移動に関してすべてを明示的に行っています。言語はコピーと参照を中心に設計されました。 C++ 11では、ものを「移動」する機能がそのシステムに接着されました。 Rust一方で、新たなスタートを切りました。


Rustにはコンストラクタがありません。コンストラクタを移動することは言うまでもありません。

移動コンストラクターは必要ありません。 Rustは、「コピーコンストラクターがない」、つまり「Copy特性を実装していない」すべてを移動します。

struct A;

fn test() {
    let a = A;
    let b = a;
    let c = a; // error, a is moved
}

Rustのデフォルトコンストラクターは、(慣例により)newと呼ばれる関連関数です。

struct A(i32);
impl A {
    fn new() -> A {
        A(5)
    }
}

より複雑なコンストラクターは、より表現力のある名前を持つ必要があります。これは、C++の名前付きコンストラクタイディオムです。


右辺値参照はサポートされていません。

これは常に要求された機能でした RFCの問題998 を参照してください。ただし、別の機能を要求している可能性があります。

struct A;

fn move_to(a: A) {
    // a is moved into here, you own it now.
}

fn test() {
    let a = A;
    move_to(a);
    let c = a; // error, a is moved
}

Rvalueパラメータを使用して関数オーバーロードを作成する方法はありません。

あなたは特性でそれを行うことができます。

trait Ref {
    fn test(&self);
}

trait Move {
    fn test(self);
}

struct A;
impl Ref for A {
    fn test(&self) {
        println!("by ref");
    }
}
impl Move for A {
    fn test(self) {
        println!("by value");
    }
}
fn main() {
    let a = A;
    (&a).test(); // prints "by ref"
    a.test(); // prints "by value"
}
44
oli_obk

Rustの移動およびコピーのセマンティクスは、C++とは大きく異なります。私はそれらを説明するために、既存の答えとは異なるアプローチを取るつもりです。


C++では、コピーはカスタムコピーコンストラクターが原因で、任意に複雑になる可能性のある操作です。 Rustは、単純な代入または引数の受け渡しのカスタムセマンティクスを必要としないため、別のアプローチをとります。

まず、Rustで渡される代入または引数は、常に単なるメモリコピーです。

let foo = bar; // copies the bytes of bar to the location of foo (might be elided)

function(foo); // copies the bytes of foo to the parameter location (might be elided)

しかし、オブジェクトがいくつかのリソースを制御している場合はどうでしょうか?単純なスマートポインターBoxを扱っているとしましょう。

let b1 = Box::new(42);
let b2 = b1;

この時点で、バイトのみがコピーされた場合、デストラクタ(Rustではdrop)が各オブジェクトに対して呼び出され、同じポインタを2回解放して未定義の動作を引き起こしませんか?

答えは、デフォルトでRustmovesです。これは、バイトを新しい場所にコピーし、その後、古いオブジェクトは削除されます。上記の2行目以降にb1にアクセスすると、コンパイルエラーが発生します。また、デストラクタは呼び出されません。値は、b2b1に移動されましたもう存在しないかもしれません。

これが移動のセマンティクスがRustで機能する方法です。バイトがコピーされ、古いオブジェクトはなくなります。

C++の移動のセマンティクスに関するいくつかの議論では、Rustの方法は「破壊的な移動」と呼ばれていました。 「移動デストラクタ」またはC++に類似した何かを追加して、同じセマンティクスを持つことができるようにする提案がありました。しかし、C++で実装されているセマンティクスを移動しても、これは行われません。古いオブジェクトは残され、そのデストラクタはまだ呼び出されています。したがって、移動操作に必要なカスタムロジックを処理するには、移動コンストラクターが必要です。移動は、特定の方法で動作することが期待される特殊なコンストラクター/割り当て演算子です。


したがって、デフォルトでは、Rustの割り当てはオブジェクトを移動し、古い場所を無効にします。しかし、多くのタイプ(整数、浮動小数点、共有参照)にはセマンティクスがあり、バイトをコピーすることは、古いオブジェクトを無視する必要なく、実際のコピーを作成するための完全に有効な方法です。そのような型は、コンパイラによって自動的に導出できるCopy特性を実装する必要があります。

#[derive(Copy)]
struct JustTwoInts {
  one: i32,
  two: i32,
}

これは、代入と引数の受け渡しが古いオブジェクトを無効にしないことをコンパイラに通知します。

let j1 = JustTwoInts { one: 1, two: 2 };
let j2 = j1;
println!("Still allowed: {}", j1.one);

ささいなコピーと破棄の必要性は相互に排他的であることに注意してください。 Copyである型Dropにすることはできません


次に、バイトをコピーするだけでは不十分なもののコピーを作成する場合はどうでしょうか。ベクトル?これには言語機能はありません。技術的には、型には、正しい方法で作成された新しいオブジェクトを返す関数が必要です。しかし慣例により、これはCloneトレイトとそのclone関数を実装することによって実現されます。実際、コンパイラはCloneの自動派生もサポートしており、すべてのフィールドを複製するだけです。

#[Derive(Clone)]
struct JustTwoVecs {
  one: Vec<i32>,
  two: Vec<i32>,
}

let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] };
let j2 = j1.clone();

また、Copyのようなコンテナーは、それ自体が複製されるときに内部で使用するため、Cloneを派生するときは常にVecも派生する必要があります。

#[derive(Copy, Clone)]
struct JustTwoInts { /* as before */ }

さて、これには欠点がありますか?はい、実際には、かなり大きな欠点が1つあります。オブジェクトを別のメモリ位置に移動するには、バイトをコピーするだけで、カスタムロジックがないため、型 それ自体への参照を含めることはできません です。実際、Rustのライフタイムシステムでは、そのような型を安全に構築することは不可能です。

しかし、私の意見では、トレードオフは価値があります。

16
Sebastian Redl

Rustは、次のような機能で移動セマンティクスをサポートしています。

  • すべてのタイプは移動可能です。

  • デフォルトでは、言語全体のどこかに値が移動されます。Copyなどの非Vecタイプの場合、Rustでのすべての移動は次のとおりです。値による引数、値を返す、代入、値によるパターンマッチング。

    Rustはデフォルトであるため、_std::move_はありません。実際に常に移動を使用しています。

  • Rustは、移動された値が使用されてはならないことを認識しています。値_x: String_があり、channel.send(x)を実行して、その値を別のスレッドに送信すると、コンパイラはxは移動されました。移動後にそれを使用しようとすると、コンパイル時のエラー「移動された値の使用」が発生します。また、誰かが値への参照(ぶら下がりポインタ)を持っている場合は、値を移動できません。

  • Rustは移動された値に対してデストラクタを呼び出さないことを認識しています。値を移動すると、クリーンアップの責任を含め、所有権が転送されます。タイプは、特別な「値が移動された」状態を表すことができる必要はありません。

  • 移動は安価ですで、パフォーマンスは予測可能です。それは基本的にmemcpyです。巨大なVecを返すのは常に速く、3つの単語をコピーするだけです。

  • The Rust標準ライブラリはあらゆる場所で移動を使用およびサポートします。すでに述べたチャネルは、移動セマンティクスを使用してスレッド間で値の所有権を安全に転送します。その他の良い点:すべてのタイプRustでコピーフリーstd::mem::swap()をサポートします。IntoFromの標準変換特性は値渡しです。Vecと他のコレクションには.drain()および.into_iter()メソッドを使用して、1つのデータ構造をスマッシュし、すべての値をその外に移動し、それらの値を使用して新しい値を作成できます。

Rustにはmoveの参照はありませんが、moveはRustの強力で中心的な概念であり、C++と同じように多くのパフォーマンス上の利点、およびその他のいくつかの利点を提供します。

2
Jason Orendorff