Подтвердить что ты не робот

Как Rust обеспечивает семантику перемещения?

На веб-сайте языка Rust утверждается, что семантика перемещения является одной из особенностей языка. Но я не вижу, как семантика перемещения реализована в 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.
  • Нет способа создавать перегрузки функций с параметрами rvalue.

Как Rust обеспечивает семантику перемещения?

4b9b3361

Ответ 1

Я думаю, что это очень распространенная проблема, когда приходит из C++. В C++ вы делаете все явно, когда дело доходит до копирования и перемещения. Язык был разработан вокруг копирования и ссылок. С C++ 11 на эту систему была наклеена способность "перемещать" вещи. С другой стороны, ржавчина взяла новое начало.


В Rust вообще нет конструкторов, не говоря уже о том, чтобы перемещать конструкторы.

Вам не нужно перемещать конструкторы. Rust перемещает все, что "не имеет конструктора копирования", a.k.a. "не реализует черту 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++


Нет поддержки для ссылок rvalue.

Это всегда была запрошенная функция, см. 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"
}

Ответ 2

Семантика перемещения и копирования ржавчины сильно отличается от 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;

На этом этапе, если копируются только байты, не будет ли вызываться деструктор (drop в Rust) для каждого объекта, таким образом освобождая один и тот же указатель дважды и вызывая неопределенное поведение?

Ответ в том, что Rust движется по умолчанию. Это означает, что он копирует байты в новое местоположение, и тогда старый объект исчезает. Доступ к b1 после второй строки выше - ошибка компиляции. И деструктор к этому не призван. Значение было перемещено в b2, и b1 также может больше не существовать.

Так работает семантика перемещения в 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 */ }

Теперь, есть ли недостатки этого? Да, на самом деле есть один довольно большой недостаток: поскольку перемещение объекта в другое место в памяти выполняется просто путем копирования байтов, а никакой настраиваемой логики тип не может иметь ссылки на себя. Фактически, система времени жизни Rust делает невозможным безопасное создание таких типов.

Но, на мой взгляд, компромисс стоит того.

Ответ 3

В C++ стандартным назначением классов и структур является мелкая копия. Значения копируются, но не данные, на которые ссылаются указатели. Таким образом, изменение одного экземпляра изменяет ссылочные данные всех копий. Значения (например, используемые для администрирования) остаются неизменными в другом случае, что, вероятно, приводит к несовместимому состоянию. Семантика движения избегает этой ситуации. Пример реализации C++ управляемого контейнера памяти с семантикой перемещения:

template <typename T>
class object
{
    T *p;
public:
    object()
    {
        p=new T;
    }
    ~object()
    {
        if (p != (T *)0) delete p;
    }
    template <typename V> //type V is used to allow for conversions between reference and value
    object(object<V> &v)      //copy constructor with move semantic
    {
        p = v.p;      //move ownership
        v.p = (T *)0; //make sure it does not get deleted
    }
    object &operator=(object<T> &v) //move assignment
    {
        delete p;
        p = v.p;
        v.p = (T *)0;
        return *this;
    }
    T &operator*() { return *p; } //reference to object  *d
    T *operator->() { return p; } //pointer to object data  d->
};

Такой объект автоматически собирается мусором и может быть возвращен из функций в вызывающую программу. Он чрезвычайно эффективен и делает то же самое, что и Rust:

object<somestruct> somefn() //function returning an object
{
   object<somestruct> a;
   auto b=a;  //move semantic; b becomes invalid
   return b;  //this moves the object to the caller
}

auto c=somefn();

//now c owns the data; memory is freed after leaving the scope