Как 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 обеспечивает семантику перемещения?
Ответы
Ответ 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