Question

C ++ et Java covariance type de retour lors de la substitution des méthodes.

Ni, cependant, soutenir contre-variance dans les types de paramètres -. A la place, il se traduit par plus de chargement (Java) ou cacher (C ++)

Pourquoi est-ce ? Il me semble qu'il n'y a pas de mal à permettre cela. I peut trouver une raison pour en Java - car il a le mécanisme « choisir-la-plus-version spécifique » pour la surcharge de toute façon - mais ne peut pas penser à une raison pour C ++

Exemple (Java):

class A {
    public void f(String s) {...}
}
class B extends A {
    public void f(Object o) {...} // Why doesn't this override A.f?
}
Était-ce utile?

La solution

Sur la question pure de contre-variance

Ajout de contre-variance à une langue ouvre beaucoup de problèmes potentiels ou des solutions impures et offre très peu d'avantages car il peut être facilement simulé sans prise en charge linguistique:

struct A {};
struct B : A {};
struct C {
   virtual void f( B& );
};
struct D : C {
   virtual void f( A& );     // this would be contravariance, but not supported
   virtual void f( B& b ) {  // [0] manually dispatch and simulate contravariance
      D::f( static_cast<A&>(b) );
   }
};

Avec un simple saut supplémentaire, vous pouvez surmonter manuellement le problème d'une langue qui ne supporte pas les contre-variance. Dans l'exemple, f( A& ) n'a pas besoin d'être virtuel, et l'appel est pleinement qualifié pour inhiber le mécanisme d'envoi virtuel.

Cette approche montre l'un des premiers problèmes qui se posent lors de l'ajout de contre-variance à une langue qui ne dispose pas d'expédition pleine dynamique:

// assuming that contravariance was supported:
struct P {
   virtual f( B& ); 
};
struct Q : P {
   virtual f( A& );
};
struct R : Q {
   virtual f( ??? & );
};

Avec contravariance en vigueur, Q::f serait une substitution de P::f, et ce serait bien que pour chaque o d'objet qui peut être un argument de P::f, ce même objet un argument valable pour Q::f . Maintenant, en ajoutant un niveau supplémentaire à la hiérarchie nous nous retrouvons avec problème de conception: est R::f(B&) une dérogation valable P::f ou devrait-il être R::f(A&)?

Sans contravariance R::f( B& ) est clairement une substitution de P::f, depuis la signature est un match parfait. Une fois que vous ajoutez contravariance au niveau intermédiaire, le problème est qu'il existe des arguments valables au niveau Q mais ne sont pas à chaque niveau de P ou R. Pour R pour répondre aux exigences de Q, le seul choix est contraint la signature être R::f( A& ), de sorte que le code suivant peut compiler:

int main() {
   A a; R r;
   Q & q = r;
   q.f(a);
}

En même temps, il n'y a rien dans la langue inhibant le code suivant:

struct R : Q {
   void f( B& );    // override of Q::f, which is an override of P::f
   virtual f( A& ); // I can add this
};

Maintenant, nous avons un drôle d'effet:

int main() {
  R r;
  P & p = r;
  B b;
  r.f( b ); // [1] calls R::f( B& )
  p.f( b ); // [2] calls R::f( A& )
}

Dans [1], il y a un appel direct à un procédé de membre de R. Depuis r est un objet local et non une référence ou pointeur, il n'y a pas de mécanisme de répartition dynamique en place et le meilleur match R::f( B& ). En même temps, [2] l'appel est fait par une référence à la classe de base, et les coups de pied du mécanisme d'expédition virtuels.

Depuis R::f( A& ) est la substitution de Q::f( A& ) qui à son tour est le remplacement de P::f( B& ), le compilateur doit appeler R::f( A& ). Bien que cela puisse être parfaitement défini dans la langue, il peut être surprenant de constater que les deux appels presque exacts [1] et [2] fait appeler des méthodes différentes, et que, dans [2] le système appellerait non meilleur match des arguments.

Bien sûr, on peut dire différemment: R::f( B& ) devrait être le remplacement correct, et non R::f( A& ). Le problème dans ce cas est:

int main() {
   A a; R r;
   Q & q = r;
   q.f( a );  // should this compile? what should it do?
}

Si vous cochez la classe Q, le code précédent est tout à fait correct: Q::f prend un A& comme argument. Le compilateur n'a aucune raison de se plaindre de ce code. Mais le problème est que, dans cette dernière hypothèse R::f prend B& et non un A& comme argument! La dérogation réelle qui serait en place ne serait pas en mesure de traiter l'argument a, même si la signature de la méthode à l'endroit de l'appel semble tout à fait correct. Ce chemin nous conduit à déterminer que le second chemin est bien pire que la première. R::f( B& ) ne saurait être une substitution de Q::f( A& ).

Suivant le principe de moindre surprise, il est beaucoup plus simple à la fois pour le compilateur et le programmeur implementor de ne pas avoir la variance contra dans les arguments de la fonction. Non pas parce qu'il est impossible, mais parce qu'il y aurait des bizarreries et des surprises dans le code, et considérant qu'il ya des simples contournements si la fonction n'est pasprésent dans la langue.

vs Hiding Surcharge

Les deux en Java et C ++, dans le premier exemple (avec A, B, C et D) supprimant l'envoi manuel [0], C::f et D::f sont différentes signatures et prend le pas sur. Dans les deux cas, ils sont en fait de la même surcharge nom de la fonction avec la légère différence qu'en raison des règles C ++ de consultation, la surcharge de C::f sera par cachée par D::f. Mais cela signifie seulement que le compilateur ne trouve pas le caché surcharge par défaut, pas que ce n'est pas présent:

int main() {
   D d; B b;
   d.f( b );    // D::f( A& )
   d.C::f( b ); // C::f( B& )
}

Et avec un léger changement dans la définition de la classe, il peut être fait pour travailler exactement les mêmes que dans Java:

struct D : C {
   using C::f;           // Bring all overloads of `f` in `C` into scope here
   virtual void f( A& );
};
int main() {
   D d; B b;
   d.f( b );  // C::f( B& ) since it is a better match than D::f( A& )
}

Autres conseils

class A {
    public void f(String s) {...}
    public void f(Integer i) {...}
}

class B extends A {
    public void f(Object o) {...} // Which A.f should this override?
}

Pour C ++, Stroustrup discute les raisons brièvement cacher dans la section 3.5.3 de la conception et l'évolution de C. Son raisonnement est (paraphrase I) que d'autres solutions soulèvent autant de questions, et il a été ainsi depuis C Avec des journées de classes.

À titre d'exemple, il donne deux classes - et une classe dérivée B. Les deux ont une fonction copie virtuelle () qui prend un pointeur de leurs types respectifs. Si nous disons:

A a;
B b;
b.copy( & a );

qui est actuellement une erreur, comme la copie de B () cache A de. Si elle était pas une erreur, seules les parties de A B pourraient être mis à jour par la fonction de copie () de A.

Encore une fois, je l'ai paraphrasé -. Si vous êtes intéressé, lisez le livre, ce qui est excellent

Bien que ce soit un beau-to-have dans toutes les langues oo, je dois encore le rencontrer son applicabilité dans mon emploi actuel.

Peut-être il n'y a pas vraiment besoin.

Merci à Donroby pour sa réponse ci-dessus -. Je suis juste étendre là-dessus

interface Alpha
interface Beta
interface Gamma extends Alpha, Beta
class A {
    public void f(Alpha a)
    public void f(Beta b)
}
class B extends A {
    public void f(Object o) {
        super.f(o); // What happens when o implements Gamma?
    }
}

Vous êtes tomber sur un problème semblable à la raison l'héritage multiple de mise en œuvre est découragée. (Si vous essayez d'appeler A.f (g) directement, vous obtiendrez une erreur de compilation.)

Merci à donroby et de réponses de David, je crois comprendre que le principal problème avec l'introduction de paramètres contre-variance est l'intégration avec le mécanisme de surcharge .

Ainsi, non seulement est-il un problème avec une seule dérogation pour plusieurs méthodes, mais aussi dans l'autre sens:

class A {
    public void f(String s) {...}
}

class B extends A {
    public void f(String s) {...} // this can override A.f
    public void f(Object o) {...} // with contra-variance, so can this!
}

Et maintenant, il y a deux remplacements valides pour la même méthode:

A a = new B();
a.f(); // which f is called?

Outre les problèmes de surcharge, je ne pouvais pas penser à autre chose.

Modifier Je l'ai trouvé depuis

scroll top