Por que o Rust não suporta o upcasting do object de traço?

Dado este código:

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

Infelizmente, não posso transmitir &Derived para o &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 

Por que é que? O vtable Derived deve referenciar os methods Base uma forma ou de outra.


Inspecionar o LLVM IR revela o seguinte:

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

Todos os vtables do Rust contêm um ponteiro para o destruidor, tamanho e alinhamento nos primeiros campos, e o subtrait vtables não os duplica ao referenciar methods do supertrait, nem usa referência indireta ao supertrait vtables. Eles só têm cópias dos pointers do método textualmente e nada mais.

Dado esse design, é fácil entender por que isso não funciona. Uma nova vtable precisaria ser construída em tempo de execução, o que provavelmente residiria na pilha, e isso não é exatamente uma solução elegante (ou ideal).

Existem algumas soluções, é claro, como adicionar methods upcast explícitos à interface, mas isso requer um pouco de clichê (ou frenesi de macro) para funcionar corretamente.

Agora, a questão é: por que ela não é implementada de alguma forma que permitiria o upcasting do object de caractere? Como, adicionando um ponteiro para vtable do supertrait na vtable do subtebra. Por enquanto, o despacho dynamic de Rust não parece satisfazer o princípio da substituição de Liskov , que é um princípio muito básico para o projeto orientado a objects.

É claro que você pode usar o despacho estático, que é de fato muito elegante para usar no Rust, mas facilmente leva ao excesso de código que às vezes é mais importante do que o desempenho computacional – como em sistemas embarcados, e os desenvolvedores Rust afirmam suportar tais casos de uso. língua. Além disso, em muitos casos, você pode usar com sucesso um modelo que não é puramente orientado a objects, o que parece ser incentivado pelo design funcional da Rust. Ainda assim, o Rust suporta muitos dos padrões OO úteis … então porque não o LSP?

Alguém sabe a razão para tal projeto?

Na verdade, acho que entendi o motivo. Eu encontrei uma maneira elegante de adicionar suporte a upcasting para qualquer traço que deseje, e dessa forma o programador é capaz de escolher se deseja adicionar aquela input vtable adicional para o traço, ou preferir não fazer isso, o que é um trade-off similar em Métodos virtuais versus não virtuais do C ++: elegância e correção do modelo versus desempenho.

O código pode ser implementado da seguinte maneira:

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

Pode-se adicionar methods adicionais para lançar um ponteiro &mut ou uma Box (que adiciona um requisito de que T deve ser um 'static tipo 'static ), mas essa é uma idéia geral. Isso permite um upcasting seguro e simples (embora não implícito) de todos os tipos derivados sem clichê para cada tipo derivado.

Eu corri para a mesma parede quando comecei com Rust. Agora, quando penso em traços, tenho uma imagem diferente em mente do que quando penso em classs.

trait X: Y {} significa que quando você implementa o traço X para struct S você também precisa implementar o traço Y para S

Claro que isso significa que um &X sabe que também é um &Y e, portanto, oferece as funções apropriadas. Seria necessário algum esforço em tempo de execução (mais referências de ponteiro) se você precisasse percorrer pointers para a primeira tabela de Y

Então, novamente, o design atual + pointers adicionais para outros vtables provavelmente não machucariam muito, e permitiriam que o lançamento fácil fosse implementado. Então talvez precisemos dos dois? Isso é algo para ser discutido em internals.rust-lang.org

A partir de junho de 2017, o status desta “coerção sub-característica” (ou “coerção super-característica”) é a seguinte:

  • Um RFC # 0401 aceito menciona isso como parte da coerção. Então essa conversão deve ser feita implicitamente.

    coerce_inner ( T ) = U onde T é uma sub-característica de U ;

  • No entanto, isso ainda não está implementado. Existe uma questão correspondente # 18600 .

Há também uma questão duplicada # 5665 . Comentários lá explicam o que impede que isso seja implementado.

  • Basicamente, o problema é como derivar vtables para super-traits. O layout atual de vtables é o seguinte (no caso x86-64):
      + ----- + ------------------------------- +
     |  0-7 | ponteiro para a function "drop glue" |
     + ----- + ------------------------------- +
     |  8-15 | tamanho dos dados |
     + ----- + ------------------------------- +
     | 16-23 | alinhamento dos dados |
     + ----- + ------------------------------- +
     | 24- | methods de Self e supertraits |
     + ----- + ------------------------------- +
    

    Não contém uma vtable para uma super-característica como uma subsequência. Temos pelo menos para ter alguns ajustes com vtables.

  • Claro que existem maneiras de atenuar esse problema, mas muitas com vantagens / desvantagens diferentes! Um tem um benefício para o tamanho vtable quando há uma inheritance de diamantes. Outro é supostamente mais rápido.

Há @typelist diz que eles prepararam um rascunho RFC que parece bem organizado, mas parece que desapareceu depois disso (novembro de 2016).