Ce qui suit montre une implémentation simple d'un affichage passant par des fonctions variadiques, donc avec un nombre arbitrairement grand de paramètres, dans trois langages distincts soit Java, C# et C++. Nous débuterons par un comparatif côte à côte des trois implémentations, suite à quoi nous reviendrons sur les détails de chacune (détail de la version C#, détail de la version C++, détail de la version Java). Les trois implémentations auront, à l'exécution, des résultats similaires (pour la version C++, cependant, il est possible d'aller beaucoup plus loin).
Placées côte à côte, les implémentations que nous examinerons dans ces trois langages vont comme suit.
Implémentation C# | Implémentation C++ | Implémentation Java |
---|---|---|
|
|
|
Exécution, implémentation C# | Exécution, implémentation C++ | Exécution, implémentation Java |
|
|
|
Ne vous fiez pas au nombre de lignes dans chaque cas pour évaluer la complexité relative de chacune des implémentations. J'ai disposé le code dans le respect (à ma connaissance) des pratiques de chaque langage, et certains (C#, par exemple) tendent à générer du code plus « vertical » alors que d'autres (Java, par exemple) mènent à du code plus « horizontal », ne serait-ce que dû aux usages quant à la disposition des accolades.
Quelques notes d'ordre général :
Pour le reste, un sympathique constat nous attend : les similitudes sont de loin plus nombreuses que les différences!
Le détail de notre implémentation à l'aide du langage C# suit.
Nous définissons tout d'abord une méthode spécialisée pour chaque type que nous souhaitons prendre en charge explicitement. Dans ces méthodes, nous affichons un message personnalisé (intégrant le nom du type dans le message). Notez que nous aurions pu atteindre un résultat semblable (mais pas identique) en utilisant la réflexivité. |
|
Nous définissons une méthode « générale » prenant tout objet en paramètre (d'où le type object), et nous testons cet objet dynamiquement pour vérifier s'il s'agit de l'un des types explicitement pris en charge. L'opérateur utilisé pour y arriver est is. Nous pouvons tester is avec des primitifs du fait que C# supporte le Boxing implicite (conversion contextuelle d'un primitif au type référence correspondant). Ces tests dynamiques sont une tare de notre programme, mais tiennent au fait que la généricité de C# passe par des classes seulement, et qu'object est le seul type parent que toutes les classes, quelles qu'elles soient, ont en commun. Les paramètres variadiques d'une même méthode en C# doivent tous être du même type. Conséquemment, pour être en mesure de passer des paramètres de type divers, il faut les placer dans ce qu'ils ont tous de commun, donc dans des références sur des object. Le cas par défaut (le else) affiche l'objet sans savoir de quoi il s'agit. Ceci sollicitera la méthode ToString() définie dans toute classe C#, qui est polymorphique. Conséquemment, si le type de l'objet inconnu a spécialisé cette méthode, alors nous aurons tout de même un affichage susceptible d'être pertinent. |
|
La méthode variadique sera Afficher(params object[] args). En C#, le mot clé params indique que le compilateur créera un tableau pour le code appelé en fonction des paramètres effectivement utilisés dans le code appelant. Puisqu'il s'agit d'un tableau, il est possible de le parcourir à l'aide de la répétitive foreach. |
|
Pour les besoins de la démonstration, nous aurons une classe X que notre système d'affichage ne connaît pas, mais dont les instances savent se « convertir » en string, ce qui nous permettra de s'en servir dans un affichage. |
|
Le programme principal instancie Variadique et en appelle la méthode variadique Afficher() avec un int, un string, un float et un X. Le compilateur construira un object[] avec ces paramètres, appliquant le Boxing sur les primitifs (le int et le float), puis la mécanique de notre programme se mettra en branle. |
|
Le détail de notre implémentation à l'aide du langage C++ suit. Tel que mentionné plus haut, cette implémentation est simpliste, et nous pourrions aller beaucoup plus loin.
Nous définissons tout d'abord une fonction spécialisée pour chaque type que nous souhaitons prendre en charge explicitement. Dans ces méthodes, nous affichons un message personnalisé (intégrant le nom du type dans le message). Notez que nous aurions pu atteindre un résultat semblable (mais pas identique) en utilisant l'opérateur statique typeid(), qui nous aurait donné accès à un std::typeinfo possédant une méthode name(). |
|
Notez la fonction afficher() générique à un seul paramètre, qui sera appelée si le type passé en paramètre n'est pas un de ceux pour lesquels il existe une implémentation spécifique. Avec C++, ceci fonctionnera autant pour les primitifs que pour les classes. Évidemment, le code ne compilera que s'il est possible de projeter un T sur un flux en sortie. |
|
La version générale prendra deux paramètres, soit un qui sera d'un type T quelconque et l'autre qui sera variadique (donc qui prendra autant de paramètres que souhaité). L'implémentation appellera afficher() à un paramètre puis afficher() avec le reste des paramètres. Lorsqu'il ne restera plus que deux paramètres à traiter, le second appel correspondra lui aussi à l'une des implémentations à un seul paramètre de la fonction afficher(). Notez la syntaxe :
|
|
Pour les besoins de la démonstration, nous aurons une classe X que notre système d'affichage ne connaît pas, mais dont il est possible de projeter les instances sur un flux en sortie, ce qui nous permettra de s'en servir dans un affichage. |
|
Le programme principal appelle la fonction afficher() avec un int, un string, un float et un X. Notez que la string est construite explicitement, car le type du littéral "J'aime mon prof" en C++ est const char* (ou char (&)[15], selon le contexte). Le compilateur générera la fonction afficher(int,string,float,X) qui appellera afficher(int), puis une fonction afficher(string,float,X) qu'il générera pour les besoins de la cause. Cette dernière appellera afficher(string), puis une fonction afficher(float,X) générée pour les besoins de la cause. Enfin, cette dernière appellera afficher(float), puis afficher(X) qui sera générée automatiquement. Avec C++, tout le travail se fait à la compilation. L'exécution n'a qu'à aller à l'essentiel. |
|
Le détail de notre implémentation à l'aide du langage Java suit.
Nous définissons tout d'abord une méthode spécialisée pour chaque type que nous souhaitons prendre en charge explicitement. Dans ces méthodes, nous affichons un message personnalisé (intégrant le nom du type dans le message). Notez que nous aurions pu atteindre un résultat semblable (mais pas identique) en utilisant la réflexivité. |
|
Nous définissons une méthode « générale » prenant tout objet en paramètre (d'où le type Object), et nous testons cet objet dynamiquement pour vérifier s'il s'agit de l'un des types explicitement pris en charge. L'opérateur utilisé pour y arriver est instanceof. Ces tests dynamiques sont une tare de notre programme, mais tiennent au fait que la généricité de Java passe par des classes seulement, et qu'Object est le seul type parent que toutes les classes, quelles qu'elles soient, ont en commun. Les paramètres variadiques d'une même méthode en Java doivent tous être du même type. Conséquemment, pour être en mesure de passer des paramètres de type divers, il faut les placer dans ce qu'ils ont tous de commun, donc dans des références sur des Object. Le cas par défaut (le else) affiche l'objet sans savoir de quoi il s'agit. Ceci sollicitera la méthode toString() définie dans toute classe Java, qui est polymorphique. Conséquemment, si le type de l'objet inconnu a spécialisé cette méthode, alors nous aurons tout de même un affichage susceptible d'être pertinent. |
|
La méthode variadique sera afficher(Object ... args). En Java, le suffixe ... appliqué au type d'un paramètre indique qu'il s'agit d'un varargs, et que le compilateur créera un tableau pour le code appelé en fonction des paramètres effectivement utilisés dans le code appelant. Puisqu'il s'agit en fait d'un tableau, il est possible de le parcourir à l'aide de la répétitive for sur un intervalle itérable. |
|
Pour les besoins de la démonstration, nous aurons une classe X que notre système d'affichage ne connaît pas, mais dont les instances savent se « convertir » en string, ce qui nous permettra de s'en servir dans un affichage. Avec Java, une classe peut être qualifiée static (membre de classe) ou non (membre d'instance). Ici, puisque main(), qui est static donc appartient à la classe, pas à une de ses instances en particulier, instanciera X, nous avons qualifié la classe X de static. |
|
Le programme principal instancie Variadique et en appelle la méthode variadique afficher() avec un int, un string, un float et un X. Le compilateur construira un object[] avec ces paramètres, appliquant le Boxing sur les primitifs (le int et le float), puis la mécanique de notre programme se mettra en branle. |
|
Vous remarquerez que les ressemblances sont beaucoup plus nombreuses que les divergencesn en particulier dans les versions Java et C# car ces langages se ressemblent philosophiquement (avec C++, une implémentation plus sérieuse tiendrait compte de choses comme la conservation des caractéristiques des paramètrs d'origine – par valeur, par référence, const ou non, déplaçable ou non, etc.).
Personnellement, j'ai écrit l'une des versions, et la traduction dans les autres langages s'est faite en quelques minutes chaque fois. Les plus importantes différences ont trait au fait que C# et Java ne permettent la généricité qu'à un degré moindre, et seulement sur les types références (en gros : les classes), même si un certain support est offert aux primitifs par voie de Boxing (construction implicite d'un Integer à partir d'un int, par exemple).