[Rust] Probleme mit Operatorüberladung in Vektor-Klasse



  • Hallo liebe Rust-Freunde.

    Ich will gerade einen Vektor in Rust implementieren und habe einige Probleme:

    use std::ops::Mul;
    
    struct Vec3<F> {
        components: [F; 3]
    }
    
    impl<F> Vec3<F> {
        fn new(x: F, y: F, z: F) -> Vec3<F> {
            Vec3 { components: [x, y, z] }
        }
    }
    
    impl<F: Copy> Mul<F> for Vec3<F> where F: Mul<F, Output=F> {
        type Output = Vec3<F>;
    
        fn mul(self, k: F) -> Vec3<F> {
            let mut components: [F; 3] = [k, k, k];
            components[0] = self.components[0]*k;
            components[1] = self.components[1]*k;
            components[2] = self.components[2]*k;
            Vec3 { components: components }
        }
    }
    
    fn main() {
        let v = Vec3::new(1i32, 2, 3);
        let vk = v*3; // ok
        let kv = 3*v; // error
    }
    

    Fragen:

    1. Wie kann ich "fn mul" schön schreiben, d.h. mit einer Schleife? Bei

    fn mul(self, k: F) -> Vec3<F> {
            let mut components: [F; 3] = [k, k, k]; // Wie initialisieren?
            for i in 0..3 {
                components[i] = self.components[i]*k;
            }
            Vec3 { components: components }
        }
    

    kriege ich

    xxxx.rs:19:29: 19:47 error: the type of this value must be known in this context
    xxxx.rs:19             components[i] = self.components[i]*k;
                                           ^~~~~~~~~~~~~~~~~~
    

    2. Bei Zeile 28 oben kriege ich folgenden Fehler:

    xxxx.rs:28:16: 28:17 error: mismatched types:
     expected `_`,
        found `Vec3<i32>`
    (expected integral variable,
        found struct `Vec3`) [E0308]
    xxxx.rs:28     let kv = 3*v; // error
                              ^
    

    Wie kann ich den richtigen Operator überladen, damit die Multiplikation kommutativ ist?



  • rusthusiast schrieb:

    Hallo liebe Rust-Freunde.

    Ich glaube nicht, dass es davon so viele hier gibt. Du bist wahrscheinlich besser auf Stackoverflow aufgehoben damit. 😉

    Die einfachste Version ohne Schleife würde so aussehen:

    fn mul(self, k: F) -> Vec3<F> {  
            Vec3 { components: [
                self.components[0] * k,
                self.components[1] * k,
                self.components[2] * k
            ] }
        }
    

    rusthusiast schrieb:

    1. Wie kann ich "fn mul" schön schreiben, d.h. mit einer Schleife? Bei

    fn mul(self, k: F) -> Vec3<F> {
            let mut components: [F; 3] = [k, k, k]; // Wie initialisieren?
            for i in 0..3 {
                components[i] = self.components[i]*k;
            }
            Vec3 { components: components }
        }
    

    kriege ich

    xxxx.rs:19:29: 19:47 error: the type of this value must be known in this context
    xxxx.rs:19             components[i] = self.components[i]*k;
                                           ^~~~~~~~~~~~~~~~~~
    

    Der einzige Unterschied, der hier zählt, ist, dass Du i zum Indizieren benutzt. Allerdings gibt es auf Arrays/Vektoren/Slices nicht nur den [] Operator, um ein einziges Element zu adressieren, sondern auch noch Überladungen zum Zerschneiden. Beispiel:

    let meinvector = vec![0,1,2,3,4,5,6,7i32];
    let ausschnitt = &meinvector[2..4];
    

    Es kommt also nicht nur usize als Index in Frage, sondern auch noch Range<usize> , RangeTo<usize> und RangeFrom<usize> . Und wie es scheint, kommt die Type-Deduction damit nicht klar. Der Compiler weiß einfach nicht, was i für einen Typ haben soll und damit, was das Ergebnis der Indizierung sein soll vom Typ her.

    Ändere mal 0..3 in 0..3us . Mit dem Suffix us sollte der Typ der range dem Compiler klar werden und damit auch von i , nämlich usize .

    Bzgl Initialisierung des Arrays kannst Du ggf. auch Default::default() benutzen, um einen Defaultwert für die Initialisierung zu erzeugen.

    Wenn Du so etwas wie Default aber nicht als Trait Bound voraussetzen willst und Dich traust, unsafe Code zu schreiben, könntest Du Dir ein Makro bauen, was es Dir erlaubt ein rohes Array ähnlich wie mit Vec::from_fn zu erzeugen und gleichzeitig zu initialisieren. Dazu brauchst Du ::std::mem::uninitialized und ::std::ptr::write . Nach der Makro-Expansion könnte das dann so aussehen:

    let arr: [F; 3] = unsafe { ::std::mem::uninitialized() };
    for i in 0..3us {
        let t = ...; // berechne deinen Wert
        unsafe { ::std::ptr::write(&mut arr[i], t); }
    }
    

    Da man diesen unsafe Code nicht wirklich überall schreiben will, ist das mit dem Makro, was das dann sicher versteckt, schon eine gute Idee. Achtung: Eine normale Zuweisung der Arrayelemente würde hier nicht notwendigerweise funktionieren. Für eine Zuweisung muss das Ziel schon gültig sein, weil es ggf ge drop t werden muss. Mit ptr::write kannst Du einen Wert an eine Speicherstelle schreiben, wo vorher nichts gültiges war, was man erst wegräumen müsste. ptr::write ist also so eine Art "placement move".

    rusthusiast schrieb:

    2. Bei Zeile 28 oben kriege ich folgenden Fehler:

    xxxx.rs:28:16: 28:17 error: mismatched types:
     expected `_`,
        found `Vec3<i32>`
    (expected integral variable,
        found struct `Vec3`) [E0308]
    xxxx.rs:28     let kv = 3*v; // error
                              ^
    

    Wie kann ich den richtigen Operator überladen, damit die Multiplikation kommutativ ist?

    Das geht leider noch nicht. Hier wird ein ähnliches Beispiel (aber ohne Generics, was es eigentlich noch einfacher machen sollte) inklusive der Problematik drumherum von einem Rust-Entwickler diskutiert:
    http://smallcultfollowing.com/babysteps/blog/2015/01/14/little-orphan-impls/
    Da geht es um "Trait Kohäränz", eine Sache, dessen Notwendigkeit ich als C++ Programmierer leider noch nicht ganz verstehe. Ich vermute, Generics sind in Rust "schwieriger", weil man den Anspruch hat, den generischen Code vollständig type-checken zu können (etwas, was Concepts Lite in C++ auch nicht kann) und weil die Type-Deduction viel mächtiger sein soll als das, was auto/template argument deduction in C++ macht.



  • Hab das hier mal eben gehackt:

    macro_rules! array {
        ($n:expr, $fun:expr) => (
            {
                let mut tmp: [_; $n] = unsafe { ::std::mem::uninitialized() };
                unsafe { overwrite_all(&mut tmp, $fun); }
                tmp
            }
        )
    }
    
    unsafe fn overwrite_all<T,F>(into: &mut[T], mut fun: F) where F: FnMut(usize)->T {
        for (idx, r) in into.iter_mut().enumerate() {
            ::std::ptr::write(r, fun(idx));
        }
    }
    
    fn main() {
        let arr = array![3, |x| (x as f64) * 3.0 + 2.25];
        println!("Hello, world! {:?}", arr)
    }
    

    Allerdings bin ich nicht 100%ig damit zufrieden; denn die vom Benutzer übergebene Closure könnte eine „Panik auslösen“. Und wenn es sich dann bei dem Elementtypen um etwas nicht-triviales mit einem Destruktor handelt, dann würden hier ggf. uninitialisierte Elemente des Arrays beim Unwinding zerstört werden, die gar nicht zerstört werden sollten, weil sie eben gar nicht erst initialisiert wurden. Man muss bei unsafe eben doch ganz schön aufpassen. Es ist also nicht „panic-safe“, weswegen ich da lieber die Finger von lassen würde.

    Um die Sache mit den panics zu umgehen, könnte man natürlich erst ein Option<T>-Array befüllen und wenn das alles geklappt hat, all die Werte in ein neues Array unwrap pen. Aber so richtig dolle gefällt mir die Lösung auch nicht.

    ::std::mem::forget könnte vielleicht auch interessant sein:

    let arr: [T; 42] = unsafe { mem::uninitialized() };  
      unsafe { mem::forget(arr); }
    

    forget konsumiert den Parameter, ruft dann aber keine Destruktoren auf, die in diesem Beispiel ja auch nicht nötig sind, da das Array gar nicht initialisiert wurde.

    Tricky.

    Es sieht für mich so aus, als ob Rust ein Äquivalent von C++'s std::aligned_storage bräuchte, damit der Compiler den rohen Speicher nicht automatisch "destrukten" will. Per RAII könnte man dann ein teilweise initialisiertes Array wieder ordentlich aufräumen…

    Gruß,
    kk


Anmelden zum Antworten