web-dev-qa-db-fra.com

Pourquoi Rust ne prend-il pas en charge la conversion ascendante d'objets de trait?

Compte tenu de ce code:

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) {}
}

Malheureusement, je ne peux pas caster &Derived En &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

Pourquoi donc? La table Derived doit référencer les méthodes Base d'une manière ou d'une autre.


L'inspection du LLVM IR révèle ce qui suit:

@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
}

Tous les vtables Rust contiennent un pointeur sur le destructeur, la taille et l'alignement dans les premiers champs, et les vtables subtrait ne les dupliquent pas lors du référencement de méthodes de supertrait, ni n'utilisent de référence indirecte pour les vtables supertrait. Ils ont juste des copies des pointeurs de méthode textuellement et rien d'autre.

Compte tenu de cette conception, il est facile de comprendre pourquoi cela ne fonctionne pas. Une nouvelle table virtuelle devrait être construite au moment de l'exécution, qui résiderait probablement sur la pile, et ce n'est pas exactement une solution élégante (ou optimale).

Il existe bien sûr des solutions de contournement, comme l'ajout de méthodes de conversion ascendante explicites à l'interface, mais cela nécessite un peu de passe-partout (ou de frénésie macro) pour fonctionner correctement.

Maintenant, la question est - pourquoi n'est-il pas implémenté d'une manière qui permettrait l'upcasting d'objets de trait? Comme, ajouter un pointeur sur la table du supertrait dans la table du sous-portrait. Pour l'instant, la répartition dynamique de Rust ne semble pas satisfaire le principe de substitution de Liskov , qui est un principe très basique pour la conception orientée objet.

Bien sûr, vous pouvez utiliser la répartition statique, qui est en effet très élégante à utiliser dans Rust, mais cela conduit facilement à une surcharge de code qui est parfois plus importante que les performances de calcul - comme sur les systèmes embarqués, et les développeurs Rust prétendent prendre en charge de tels cas d'utilisation de la langue. De plus, dans de nombreux cas, vous pouvez utiliser avec succès un modèle qui n'est pas purement orienté objet, ce qui semble être encouragé par la conception fonctionnelle de Rust. Néanmoins, Rust prend en charge de nombreux modèles OO utiles ... alors pourquoi pas le LSP?

Quelqu'un connaît-il la justification d'une telle conception?

44
kFYatek

En fait, je pense avoir compris la raison. J'ai trouvé un moyen élégant d'ajouter un support de conversion ascendante à tout trait qui le désire, et de cette façon, le programmeur est en mesure de choisir d'ajouter cette entrée vtable supplémentaire au trait, ou préfère ne pas le faire, qui est un compromis similaire à celui de Méthodes virtuelles vs non virtuelles de C++: élégance et exactitude du modèle vs performances.

Le code peut être implémenté comme suit:

trait Base: AsBase {
    // ...
}

trait AsBase {
    fn as_base(&self) -> &Base;
}

impl<T: Base> AsBase for T {
    fn as_base(&self) -> &Base {
        self
    }
}

On peut ajouter des méthodes supplémentaires pour lancer un &mut pointeur ou Box (qui ajoute que T doit être un 'static type), mais c'est une idée générale. Cela permet une conversion ascendante sûre et simple (bien que non implicite) de chaque type dérivé sans passe-partout pour chaque type dérivé.

45
kFYatek

Depuis juin 2017, le statut de cette "coercition de sous-trait" (ou "coercition de super-trait") est le suivant:

  • Un RFC accepté # 0401 le mentionne comme faisant partie de la coercition. Cette conversion doit donc être effectuée implicitement.

    coerce_inner (T) = UT est un sous-trait de U;

  • Cependant, cela n'est pas encore implémenté. Il y a un problème correspondant # 186 .

Il existe également un problème en double # 5665 . Les commentaires expliquent ce qui empêche sa mise en œuvre.

Là --- (@ typelist dit qu'ils ont préparé n projet de RFC qui a l'air bien organisé, mais ils ont l'air d'avoir disparu après cela (novembre 2016).

22
Masaki Hara

Je suis tombé sur le même mur quand j'ai commencé avec Rust. Maintenant, quand je pense aux traits, j'ai une image différente à l'esprit que quand je pense aux cours.

trait X: Y {} signifie que lorsque vous implémentez le trait X pour la structure S vous avez également besoin pour implémenter le trait Y pour S.

Bien sûr, cela signifie qu'un &X sait que c'est aussi un &Y, et propose donc les fonctions appropriées. Cela nécessiterait un effort d'exécution (plus de déréférences de pointeur) si vous deviez d'abord traverser des pointeurs vers la table de Y.

Là encore, la conception actuelle + des pointeurs supplémentaires vers d'autres vtables ne feraient probablement pas beaucoup de mal et permettraient une mise en œuvre facile. Alors peut-être que nous avons besoin des deux? C'est quelque chose à discuter sur internals.Rust-lang.org

17
oli_obk