Перемещение против копирования в ржавчине

Скажем, у меня есть эта структура

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
}

Если передать это функции, она неявно скопирована. Теперь иногда я читаю, что некоторые значения не копируются и, следовательно, вынуждены перемещаться. Интересно, возможно ли сделать это struct Triplet не скопируемым каким-то образом? Подобно внедрению некоторой дополнительной черты, которая сообщает ржавчине, что это будет не скопировано и, следовательно, должно двигаться вместо этого.

Я читаю везде, что нужно реализовать черту Clone, чтобы скопировать вещи, которые неявно копируются, но я никогда не читал о том, что происходит с тем, что неявно скопируется и делает он не копируется, поэтому вместо этого он перемещается.

Это даже имеет смысл?

Ответ 1

Предисловие: этот ответ был написан до встроенных признаков, особенно Copy аспектов были реализованы. Я использовал кавычки блока, чтобы указать разделы, которые применяются только к старой схеме (той, которая применялась при задании вопроса).


Старый. Чтобы ответить на основной вопрос, вы можете добавить поле маркера, сохраняющее значение NoCopy. Например.

struct Triplet {
    one: int,
    two: int,
    three: int,
    _marker: NoCopy
}

Вы также можете сделать это, используя деструктор (используя Drop trait), но использование типов маркеров является предпочтительным, если деструктор ничего не делает.

Теперь типы теперь перемещаются по умолчанию, то есть когда вы определяете новый тип, он не реализует Copy, если вы явно не реализуете его для своего типа:

struct Triplet {
    one: i32,
    two: i32,
    three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move

Реализация может существовать только в том случае, если каждый тип, содержащийся в новом struct или enum, сам Copy. Если нет, компилятор напечатает сообщение об ошибке. Он также может существовать только в том случае, если тип не имеет реализации Drop.


Чтобы ответить на вопрос, который вы не спросили... "что с движениями и копией?":

Во-первых, я определяю две разные копии:

  • копия байта, которая просто мелко копирует объект побайтно, а не следующие указатели, например. если у вас есть (&usize, u64), это 16 байт на 64-битном компьютере, а мелкая копия будет брать эти 16 байт и реплицировать их значение в другой 16-разрядной части памяти, не касаясь usize на другой конец &. То есть, это эквивалентно вызову memcpy.
  • семантическая копия, дублирующая значение для создания нового (несколько) независимого экземпляра, который можно безопасно использовать отдельно для старого. Например. семантическая копия Rc<T> включает только увеличение счетчика ссылок, а семантическая копия Vec<T> предполагает создание нового выделения, а затем семантическое копирование каждого сохраненного элемента из старого в новое. Это могут быть глубокие копии (например, Vec<T>) или неглубокие (например, Rc<T> не касается сохраненного T), Clone определяется как минимальный объем работы, требуемый для семантической копирования значения типа T изнутри a &T в T.

Rust похож на C, каждое использование значения значением является байтовой копией:

let x: T = ...;
let y: T = x; // byte copy

fn foo(z: T) -> T {
    return z // byte copy
}

foo(y) // byte copy

Они являются байтовыми копиями независимо от того, перемещается ли или нет T или "неявно скопируется". (Чтобы быть ясным, они не обязательно буквально побайтовые копии во время выполнения: компилятор может свободно оптимизировать копии, если поведение кода сохраняется.)

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

{
    let v: Vec<u8> = vec![1, 2, 3];
    let w: Vec<u8> = v;
} // destructors run here

Если w был просто простой байтовой копией v, тогда было бы два вектора, указывающих на одно и то же распределение, как с деструкторами, освобождающими его... вызывая a двойной бесплатный, что является проблемой. NB. Это было бы прекрасно, если бы мы сделали семантическую копию v в w, так как тогда w будет ее собственным независимым Vec<u8>, а деструкторы не будут топтаться друг от друга.

Здесь есть несколько возможных исправлений:

  • Пусть программист справится с этим, как и C. (там нет деструкторов в C, так что это не так плохо... вы просто останетесь с утечками памяти.: P)
  • Выполнять семантическую копию неявно, так что w имеет свое собственное выделение, например С++ с его конструкторами копирования.
  • Regard by-value использует в качестве передачи права собственности, так что v больше не может использоваться и не выполняет его деструктор.

Последнее - это то, что делает Rust: перемещение - это просто использование по значению, когда источник статически недействителен, поэтому компилятор предотвращает дальнейшее использование недопустимой памяти.

let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value

Типы, которые имеют деструкторы, должны перемещаться при использовании по-значению (например, при копировании байта), поскольку они имеют управление/право собственности на какой-либо ресурс (например, распределение памяти или дескриптор файла), и очень маловероятно, что байт-копия будет правильно дублируйте это право собственности.

"Ну... какая неявная копия?"

Подумайте о примитивном типе типа u8: байтовая копия проста, просто скопируйте один байт, а семантическая копия будет такой же простой, скопируйте один байт. В частности, байт-копия является семантической копией... У Rust даже есть встроенный признак Copy, который фиксирует, какие типы имеют идентичные семантические и байтовые копии.

Следовательно, для этих Copy типов использование по-значения также является автоматически семантическими копиями, и поэтому совершенно безопасно продолжать использовать источник.

let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine

Старый: маркер NoCopy переопределяет автоматическое поведение компилятора, предполагая, что типы, которые могут быть Copy (т.е. содержащие только агрегаты примитивов и &), Copy. Однако это будет изменяться, когда будет реализовано встроенные функции.

Как уже упоминалось выше, встроенные встроенные функции реализованы, поэтому компилятор больше не имеет автоматического поведения. Однако правило, используемое для автоматического поведения в прошлом, - это те же правила для проверки того, является ли законным применение Copy.

Ответ 2

Самый простой способ - вставить что-то в свой тип, который не может быть скопирован.

Стандартная библиотека предоставляет "тип маркера" для именно этого прецедента: NoCopy. Например:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}