Czy można uczynić czcionkę tylko ruchomą, a nie kopiowalną?

96

Uwaga redaktora : to pytanie zostało zadane przed Rust 1.0, a niektóre stwierdzenia w nim niekoniecznie są prawdziwe w Rust 1.0. Niektóre odpowiedzi zostały zaktualizowane w celu uwzględnienia obu wersji.

Mam tę strukturę

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

Jeśli przekażę to do funkcji, jest ona niejawnie kopiowana. Czasami czytam, że niektórych wartości nie da się skopiować i dlatego trzeba je przenieść.

Czy byłoby możliwe uniemożliwienie Tripletkopiowania tej struktury ? Na przykład, czy byłoby możliwe zaimplementowanie cechy, która uczyniłaby Tripletniekopiowalną, a zatem „ruchomą”?

Czytałem gdzieś, że trzeba zaimplementować Clonecechę kopiowania rzeczy, które nie są w sposób dorozumiany kopiowalne, ale nigdy nie czytałem o czymś odwrotnym, czyli o posiadaniu czegoś, co jest niejawnie kopiowalne i uniemożliwiające kopiowanie, aby zamiast tego się poruszało.

Czy to w ogóle ma sens?

Christoph
źródło
1
paulkoerbitz.de/posts/… . Dobre wyjaśnienia, dlaczego przenoszenie się, a nie kopiowanie.
Sean Perry

Odpowiedzi:

165

Przedmowa : Ta odpowiedź została napisana przed opt-in wbudowaną cech -specifically te Copyaspekty -were realizowanych. Użyłem cudzysłowów blokowych, aby wskazać sekcje, które dotyczyły tylko starego schematu (tego, który był stosowany, gdy zadawano pytanie).


Stare : Aby odpowiedzieć na podstawowe pytanie, możesz dodać pole znacznika przechowujące NoCopywartość . Na przykład

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

Możesz to również zrobić, mając destruktor (poprzez implementację Dropcechy ), ale używanie typów znaczników jest preferowane, jeśli destruktor nic nie robi.

Typy są teraz przenoszone domyślnie, to znaczy, gdy definiujesz nowy typ, nie jest on implementowany, Copychyba że jawnie zaimplementujesz go dla swojego typu:

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

Implementacja może istnieć tylko wtedy, gdy każdy typ zawarty w new structlub enumjest sobą Copy. Jeśli nie, kompilator wydrukuje komunikat o błędzie. Może również istnieć tylko wtedy, gdy typ nie ma Dropimplementacji.


Aby odpowiedzieć na pytanie, którego nie zadałeś ... „o co chodzi z przenoszeniem i kopiowaniem?”:

Najpierw zdefiniuję dwie różne „kopie”:

  • kopia bajt , który jest po prostu płytko kopiując bajt bajt po obiekcie, nie następujące wskaźniki, na przykład, jeśli masz (&usize, u64), to 16 bajty na komputerze 64-bitowym, a płytkie kopia będzie przy tych 16 bajtów i replikowania swoich wartość w innym 16-bajtowym fragmencie pamięci, bez dotykania usizedrugiego końca pliku &. Oznacza to, że jest to równoważne z dzwonieniem memcpy.
  • semantyczny kopiowania , powielania wartość, aby utworzyć nową (nieco) niezależnej instancji, które mogą być bezpiecznie używane oddzielnie do starego. Np. Semantyczna kopia a Rc<T>wymaga po prostu zwiększenia liczby odniesień, a semantyczna kopia a Vec<T>polega na utworzeniu nowej alokacji, a następnie semantycznym kopiowaniu każdego przechowywanego elementu ze starego do nowego. Mogą to być kopie głębokie (np. Vec<T>) Lub płytkie (np. Rc<T>Nie dotykają przechowywanych T), Clonesą luźno definiowane jako najmniejsza ilość pracy wymagana do semantycznego skopiowania wartości typu Tz wnętrza a &Tdo T.

Rust jest podobny do C, każde użycie wartości według wartości jest kopią bajtową:

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

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

foo(y) // byte copy

Są to kopie bajtowe, niezależnie od tego, czy są Tprzenoszone, czy też są „niejawnie kopiowalne”. (Aby było jasne, nie zawsze są to kopie bajt po bajcie w czasie wykonywania: kompilator może optymalizować kopie, jeśli zachowanie kodu jest zachowane.)

Istnieje jednak podstawowy problem z kopiami bajtowymi: kończy się to ze zduplikowanymi wartościami w pamięci, co może być bardzo złe, jeśli mają destruktory, np.

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

Gdyby wbyła to zwykła kopia bajtowa, vbyłyby dwa wektory wskazujące na tę samą alokację, oba z destruktorami, które ją zwalniają ... powodując podwójne zwolnienie , co jest problemem. NB. Byłoby to całkowicie w porządku, gdybyśmy zrobili semantyczną kopię programu vinto w, ponieważ wtedy wbyłyby jego własne niezależne, Vec<u8>a destruktory nie deptałyby się nawzajem.

Istnieje kilka możliwych poprawek:

  • Pozwól programiście zająć się tym, jak C. (w C nie ma destruktorów, więc nie jest tak źle ... zamiast tego zostajesz z wyciekami pamięci.: P)
  • Wykonaj kopię semantyczną niejawnie, tak aby wmiała własną alokację, podobnie jak C ++ z konstruktorami kopiowania.
  • Traktuj wykorzystanie według wartości jako przeniesienie własności, więc vnie można go już używać i nie można uruchomić jego destruktora.

Ostatnim jest to, co robi Rust: ruch to tylko użycie wartości, w którym źródło jest statycznie unieważnione, więc kompilator zapobiega dalszemu używaniu niepoprawnej pamięci.

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

Typy, które mają destruktory, muszą się przesuwać, gdy są używane przez wartość (inaczej podczas kopiowania bajtu), ponieważ mają zarządzanie / własność jakiegoś zasobu (np. Alokację pamięci lub uchwyt pliku) i jest bardzo mało prawdopodobne, aby kopia bajtowa poprawnie to powieliła własność.

„Cóż… co to jest ukryta kopia?”

Pomyśl o typie pierwotnym, takim jak u8: kopia bajtowa jest prosta, po prostu skopiuj pojedynczy bajt, a kopia semantyczna jest równie prosta, skopiuj pojedynczy bajt. W szczególności kopia bajtowa jest kopią semantyczną ... Rust ma nawet wbudowaną cechę,Copy która przechwytuje, które typy mają identyczne kopie semantyczne i bajtowe.

W związku z tym dla tych Copytypów zastosowań według wartości są również automatycznie kopiami semantycznymi, więc dalsze korzystanie ze źródła jest całkowicie bezpieczne.

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

Stary : NoCopyZnacznik przesłania automatyczne zachowanie kompilatora polegające na założeniu, że typy, które mogą być Copy(tj. Zawierają tylko agregaty prymitywów i &), są Copy. Jednak ulegnie to zmianie, gdy zaimplementowane zostaną wbudowane cechy optyczne .

Jak wspomniano powyżej, wbudowane cechy opt-in są zaimplementowane, więc kompilator nie ma już automatycznego zachowania. Jednak reguła stosowana w przeszłości do automatycznego zachowania to te same reguły, które służą do sprawdzania, czy ich wdrożenie jest legalne Copy.

huon
źródło
@dbaupp: Czy zdarzyło Ci się wiedzieć, w której wersji Rusta pojawiły się wbudowane cechy opt-in? Myślę, że 0,10.
Matthieu M.
@MatthieuM. nie jest jeszcze zaimplementowany, a ostatnio pojawiły się pewne propozycje zmian w projekcie wbudowanych opcji opt-in .
huon
Myślę, że stary cytat powinien zostać usunięty.
Stargateur,
1
# [derive (Copy, Clone)] powinno być używane na Triplet nie impl
shadowbq
6

Najłatwiej jest osadzić w typie coś, czego nie można skopiować.

Biblioteka standardowa zapewnia „typ znacznika” dokładnie dla tego przypadku użycia: NoCopy . Na przykład:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}
BurntSushi5
źródło
15
Nie dotyczy to Rust> = 1.0.
malbarbo