Eh bien, je dois donner une réponse inattendue ici ! Dennycrane a dit que vous pouvez le faire :
virtual int CompareTo(IComparableObject const &obj)=0;
mais ce n'est pas correct non plus. Oh oui, ça compile, mais c'est inutile parce que ça ne peut jamais être implémenté correctement.
Ce problème est fondamental pour l'effondrement de l'orientation objet (typée statiquement), il est donc vital que les programmeurs utilisant l'OO reconnaissent ce problème. Le problème a un nom, il s'appelle le problème de la covariance et elle détruit complètement l'OO en tant que paradigme général de programmation, c'est-à-dire une façon de représenter et de mettre en œuvre de manière indépendante des abstractions générales.
Cette explication sera un peu longue et peu soignée, alors soyez indulgent avec moi et essayez de lire entre les lignes.
Tout d'abord, une classe abstraite avec une méthode virtuelle pure ne prenant aucun argument peut être facilement implémentée dans n'importe quelle classe dérivée, puisque la méthode a accès aux variables de données non statiques de la classe dérivée via le pointeur this. Le pointeur this a le type d'un pointeur vers la classe dérivée, et on peut donc dire qu'il varie avec la classe, en fait c'est covariant avec le type de classe dérivée.
Je qualifie ce type de polymorphisme de premier ordre, il supporte clairement les prédicats de répartition sur le type de l'objet. En effet, le type de retour d'une telle méthode peut également varier vers le bas avec le type de l'objet et de la classe, c'est-à-dire que le type de retour est covariant .
Maintenant, je vais généraliser l'idée d'une méthode sans arguments pour permettre des arguments scalaires arbitraires (comme les ints) en affirmant que cela ne change rien : il s'agit simplement d'une famille de méthodes indexées par le type scalaire. La propriété importante ici est que le type scalaire est fermé. Dans une classe dérivée, exactement le même type scalaire doit être utilisé. en d'autres termes, le type est invariant .
L'introduction générale de paramètres invariants dans une fonction virtuelle permet toujours le polymorphisme, mais le résultat est toujours de premier ordre.
Malheureusement, ces fonctions ont une utilité limitée, bien qu'elles soient très utiles lorsque l'abstraction n'est que de premier ordre : les pilotes de périphériques en sont un bon exemple.
Mais que faire si l'on veut modéliser quelque chose qui soit réellement intéressant, c'est-à-dire qui soit au moins une relation ?
La réponse à cette question est : vous ne pouvez pas le faire. Il s'agit d'un fait mathématique qui n'a rien à voir avec le langage de programmation utilisé. Supposons que vous ayez une abstraction pour, disons, des nombres, et que vous vouliez ajouter un nombre à un autre nombre, ou les comparer (comme dans l'exemple de l'OP). En ignorant la symétrie, si vous avez N implémentations, vous devrez écrire N^2 fonctions pour effectuer les opérations. Si vous ajoutez une nouvelle implémentation de l'abstraction, vous devez écrire N+1 nouvelles fonctions.
Maintenant, j'ai la première preuve que l'OO est foutu : vous ne pouvez pas faire rentrer N^2 méthodes dans un schéma de répartition virtuelle parce qu'un tel schéma est linéaire. N classes vous donnent N méthodes que vous pouvez implémenter et pour N>1, N^2 > N, donc OO est foutu, QED.
Dans un contexte C++, vous pouvez voir le problème : considérez :
struct MyComparable : IComparableObject {
int CompareTo(IComparableObject &other) { .. }
};
Arggg ! On est foutu ! On ne peut pas remplir le ..
car nous n'avons qu'une référence à une abstraction, qui ne contient aucune donnée à laquelle comparer. Bien sûr, cette doit être le cas, car il existe un nombre ouvert/indéterminé/infini d'implémentations possibles. Il n'y a pas de moyen possible d'écrire une seule routine de comparaison comme un axiome.
Bien sûr, si vous avez diverses routines de propriétés, ou une représentation universelle commune, vous pouvez le faire, mais cela ne compte pas, car alors le mappage vers la représentation universelle est sans paramètre et donc l'abstraction n'est que de premier ordre. Par exemple, si vous avez diverses représentations d'entiers et que vous les additionnez en les convertissant toutes deux en type de données mpz de GNU gmp, vous utilisez alors deux fonctions de projection covariantes et une seule fonction de comparaison ou d'addition globale non polymorphe.
Ce n'est pas un contre-exemple, c'est une non-solution du problème, qui est de représenter une relation ou une méthode qui est covariante dans au moins deux variables (au moins soi et l'autre).
Vous pouvez penser que vous pouvez résoudre ce problème avec :
struct MyComparable : IComparableObject {
int CompareTo(MyComparable &other) { .. }
};
Après tout, vous pouvez implémenter cette interface car vous connaissez la représentation de l'autre maintenant, puisque c'est MyComparable.
Ne riez pas de cette solution, car c'est exactement ce que Bertrand Meyer a fait en Eiffel, et c'est ce que beaucoup de gens font en C++ avec une petite modification pour essayer de contourner le fait que ce n'est pas sûr au niveau du type et que cela ne surcharge pas réellement la fonction de la classe de base :
struct MyComparable : IComparableObject {
int CompareTo(IComparableObject &other) {
try
MyComparable &sibling = dynamic\_cast(other);
...
catch (..) { return 0; }
}
};
Ce n'est pas une solution. Elle dit que deux choses ne sont pas égales simplement parce qu'elles ont des représentations différentes. Cela ne répond pas à l'exigence, qui est de comparer deux choses dans l'abstrait. Deux nombres, par exemple, ne peuvent pas ne pas être égaux simplement parce que la représentation utilisée est différente : zéro est égal à zéro, même si l'un est un mpz et l'autre un int. Rappelez-vous : l'idée est de représenter correctement une abstraction, ce qui signifie que le comportement doit dépendre uniquement de la valeur abstraite, et non des détails d'une implémentation particulière.
Certaines personnes ont essayé la double expédition. Il est clair que cela ne peut pas fonctionner non plus. Il n'y a pas d'échappatoire possible au problème de base : on ne peut pas mettre un carré dans une ligne. La répartition des fonctions virtuelles est linéaire, les problèmes de second ordre sont quadratiques, et l'OO ne peut donc pas représenter les problèmes de second ordre.
Je tiens à préciser que le C++ et les autres langages OO à typage statique ne fonctionnent pas, no parce qu'ils ne peuvent pas résoudre ce problème, parce qu'il ne peut pas être résolu, et ce n'est pas un problème : c'est un simple fait. La raison pour laquelle ces langages et le paradigme OO en général sont cassés est qu'ils promesse pour fournir des abstractions générales et ensuite échouer à le faire. Dans le cas du C++, c'est la promesse :
struct IComparableObject { virtual int CompareTo(IComparableObject obj)=0; };
et c'est là que le contrat implicite est rompu :
struct MyComparable : IComparableObject {
int CompareTo(IComparableObject &other) { throw 00; }
};
parce que l'implémentation que j'ai donnée ici est effectivement la seule possible.
Bien avant de partir, vous pouvez vous demander : Qu'est-ce que la la bonne voie (TM) . La réponse est : utiliser la programmation fonctionnelle. En C++, cela signifie des modèles.
template<class T, class U> int compare(T,U);
Donc si vous avez N types à comparer, et que vous comparez réellement toutes les combinaisons, alors oui, en effet, vous devez fournir N^2 spécialisations. Ce qui montre les modèles livrer sur la promesse, au moins sur ce point. C'est un fait : vous ne pouvez pas dispatcher au moment de l'exécution sur un ensemble ouvert de types si la fonction est variante dans plus d'un paramètre.
BTW : au cas où vous ne seriez pas convaincu par la théorie il suffit de regarder la bibliothèque standard ISO C++ et de voir combien de polymorphisme de fonction virtuelle est utilisé là-bas, par rapport à la programmation fonctionnelle avec des modèles
Enfin, notez bien que je ne dis pas que les classes et autres sont inutiles, j'utilise moi-même le polymorphisme des fonctions virtuelles : Je dis que cela se limite à des problèmes particuliers et n'est pas une manière générale de représenter les abstractions, et ne mérite donc pas d'être appelé un paradigme.