Perché Rust non supporta l’upcast degli oggetti tratto?

Dato questo codice:

trait Base { fn a(&self); fn b(&self); fn c(&self); fn d(&self); } trait Derived : Base { fn e(&self); fn f(&self); fn g(&self); } struct S; impl Derived for S { fn e(&self) {} fn f(&self) {} fn g(&self) {} } impl Base for S { fn a(&self) {} fn b(&self) {} fn c(&self) {} fn d(&self) {} } 

Purtroppo, non posso eseguire il cast &Derived su &Base :

 fn example(v: &Derived) { v as &Base; } 
 error[E0605]: non-primitive cast: `&Derived` as `&Base` --> src/main.rs:30:5 | 30 | v as &Base; | ^^^^^^^^^^ | = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait 

Perché? Il vtable Derived deve fare riferimento ai metodi Base in un modo o nell’altro.


L’ispezione di LLVM IR rivela quanto segue:

 @vtable4 = internal unnamed_addr constant { void (i8*)*, i64, i64, void (%struct.S*)*, void (%struct.S*)*, void (%struct.S*)*, void (%struct.S*)* } { void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE, i64 0, i64 1, void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE, void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE, void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE, void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE } @vtable26 = internal unnamed_addr constant { void (i8*)*, i64, i64, void (%struct.S*)*, void (%struct.S*)*, void (%struct.S*)*, void (%struct.S*)*, void (%struct.S*)*, void (%struct.S*)*, void (%struct.S*)* } { void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE, i64 0, i64 1, void (%struct.S*)* @_ZN9S.Derived1e20h9992ddd0854253d1WaaE, void (%struct.S*)* @_ZN9S.Derived1f20h849d0c78b0615f092aaE, void (%struct.S*)* @_ZN9S.Derived1g20hae95d0f1a38ed23b8aaE, void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE, void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE, void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE, void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE } 

Tutti i vtables di Rust contengono un puntatore al distruttore, la dimensione e l’allineamento nei primi campi, e i vtables di sottotitoli non li duplicano quando fanno riferimento a metodi di supertrait, né usano riferimenti indiretti a vtrables di supertrait. Hanno solo copie dei puntatori del metodo testualmente e nient’altro.

Dato questo disegno, è facile capire perché questo non funziona. Un nuovo vtable dovrebbe essere costruito in fase di runtime, che probabilmente risiederà nello stack e non è esattamente una soluzione elegante (o ottimale).

Ci sono alcuni workaround, ovviamente, come aggiungere metodi upcast espliciti all’interfaccia, ma questo richiede un po ‘di boilerplate (o macro frenesia) per funzionare correttamente.

Ora, la domanda è: perché non è implementata in qualche modo che consentirebbe l’upcast degli oggetti tratto? Come, aggiungendo un puntatore al vtable della supertrait nel vtable della subtrait. Per ora, la spedizione dynamic di Rust non sembra soddisfare il principio di sostituzione di Liskov , che è un principio molto basilare per la progettazione orientata agli oggetti.

Ovviamente è ansible utilizzare il dispatch statico, che è davvero molto elegante da utilizzare in Rust, ma porta facilmente a code bloat che a volte è più importante delle prestazioni computazionali – come nei sistemi embedded, e gli sviluppatori di Rust sostengono di supportare tali casi d’uso del linguaggio. Inoltre, in molti casi è ansible utilizzare con successo un modello che non è puramente orientato agli oggetti, che sembra essere incoraggiato dal design funzionale di Rust. Ancora, Rust supporta molti dei modelli OO utili … quindi perché non l’LSP?

Qualcuno conosce la logica di tale progettazione?

In realtà, penso di aver capito la ragione. Ho trovato un modo elegante per aggiungere il supporto upcasting a qualsiasi tratto che lo desideri, e in questo modo il programmatore è in grado di scegliere se aggiungere quella voce addizionale vtable al tratto, o preferire di no, che è un compromesso simile come in I metodi virtuali e non virtuali di C ++: eleganza e correttezza del modello rispetto alle prestazioni.

Il codice può essere implementato come segue:

 trait Base: AsBase { // ... } trait AsBase { fn as_base(&self) -> &Base; } impl AsBase for T { fn as_base(&self) -> &Base { self } } 

Si possono aggiungere metodi aggiuntivi per lanciare un puntatore &mut o una Box (che aggiunge un requisito che T deve essere un 'static tipo 'static ), ma questa è un’idea generale. Ciò consente un upcasting sicuro e semplice (anche se non implicito) di ogni tipo derivato senza boilerplate per ogni tipo derivato.

Mi sono imbattuto nello stesso muro quando ho iniziato con Rust. Ora, quando penso ai tratti, ho in mente un’immagine diversa rispetto a quando penso alle lezioni.

trait X: Y {} significa che quando si implementa il tratto X per struct S è necessario implementare il tratto Y per S

Ovviamente questo significa che a &X sa anche che è un &Y , e quindi offre le funzioni appropriate. Richiederebbe uno sforzo di runtime (più dereferenze del puntatore) se fosse necessario passare prima i puntatori al vtable di Y

Inoltre, il progetto attuale + ulteriori puntatori ad altri vtables probabilmente non farebbe molto male, e permetterebbe un cast facile da implementare. Quindi forse abbiamo bisogno di entrambi? Questo è qualcosa da discutere su internals.rust-lang.org

A partire da giugno 2017, lo stato di questa “coercizione sotto-tratti” (o “coercizione super-trait”) è il seguente:

  • Un RFC # 0401 accettato menziona questo come una parte della coercizione. Quindi questa conversione dovrebbe essere fatta implicitamente.

    coerce_inner ( T ) = U dove T è un sotto-tratto di U ;

  • Tuttavia, questo non è ancora stato implementato. C’è un problema corrispondente # 18600 .

C’è anche un problema duplicato # 5665 . I commenti spiegano cosa impedisce che questo venga implementato.

  • Fondamentalmente, il problema è come derivare i vtables per i super-tratti. Il layout corrente dei vtables è il seguente (nel caso x86-64):
      + ----- + ------------------------------- +
     |  0-7 | puntatore alla funzione "drop glue" |
     + ----- + ------------------------------- +
     |  8-15 | dimensione dei dati |
     + ----- + ------------------------------- +
     | 16-23 | allineamento dei dati |
     + ----- + ------------------------------- +
     | 24- | metodi di Sé e supertratti |
     + ----- + ------------------------------- +
    

    Non contiene un vtable per un super-tratto come una sottosequenza. Dobbiamo avere almeno qualche ritouch con i vtables.

  • Naturalmente ci sono modi per mitigare questo problema, ma molti con diversi vantaggi / svantaggi! Uno ha un vantaggio per le dimensioni vtable quando c’è un’eredità di diamanti. Un altro dovrebbe essere più veloce.

Lì @typelist dice che hanno preparato una bozza di RFC che sembra ben organizzata, ma sembra che siano scomparsi dopo questo (novembre 2016).