Comparatif d'implémentations, fonctions variadiques

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).

Comparatif côte à côte

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
using System;

namespace Variadique
{
   class Variadique
   {
      public void Afficher(float f)
      {
         Console.WriteLine("float de valeur {0}", f);
      }
      public void Afficher(int i)
      {
         Console.WriteLine("int de valeur {0}", i);
      }
      public void Afficher(string s)
      {
         Console.WriteLine("string de valeur \"{0}\"", s);
      }
      public void Afficher(object arg)
      {
         if (arg is float)
         {
            Afficher((float)arg);
         }
         else if (arg is int)
         {
            Afficher((int)arg);
         }
         else if (arg is string)
         {
            Afficher((string)arg);
         }
         else
         {
            Console.WriteLine("objet de type effectif inconnu et de valeur \"{0}\"", arg);
         }
      }
      public void Afficher(params object[] args)
      {
         foreach (object arg in args)
         {
            Afficher(arg);
         }
      }
   }
   class X
   {
      public override string ToString()
      {
         return "Je suis un X";
      }
   }
   class Program
   {
      static void Main(string[] args)
      {
         Variadique vq = new Variadique();
         vq.Afficher(3, "J'aime mon prof", 3.14159f, new X());
      }
   }
}
#include <iostream>
#include <string>
using namespace std;

void afficher(float f)
{
   cout << "float de valeur " << f << endl;
}
void afficher(int i)
{
   cout << "int de valeur " << i << endl;
}
void afficher(string s)
{
    cout << "string de valeur \"" << s << "\"" << endl;
}
template <class T>
   void afficher(const T &arg)
   {
       cout << "objet de type effectif inconnu et de valeur \"" << arg << "\"" << endl;
   }

template <class T, class ... Args>
   void afficher(const T &arg, Args ... args)
   {
      afficher(arg);
      afficher(args...);
   }
class X
{
   friend ostream & operator<< (ostream &os, const X &)
      { return os << "Je suis un X"; }
};

int main()
{
   afficher(3, string("J'aime mon prof"), 3.14159f, X());
}
import java.io.*;

public class Variadique {
   public void afficher(float f) {
      System.out.println("float de valeur " + f);
   }
   public void afficher(int i) {
      System.out.println("int de valeur " + i);
   }
   public void afficher(String s) {
      System.out.println("String de valeur \"" + s + "\"");
   }
   public void afficher(Object arg) {
      if(arg instanceof Float) {
         afficher((float) arg);
      } else if (arg instanceof Integer) {
         afficher((int) arg);
      } else if (arg instanceof String) {
         afficher((String) arg);
      } else {
         System.out.println("objet de type effectif inconnu et de valeur \"" + arg + "\"");
      }
   }
   public void afficher(Object ... args) {
      for(Object arg : args) {
         afficher(arg);
      }
   }
   static class X {
      public String toString() {
         return "Je suis un X";
      }
   }
   public static void main(String [] args) {
      Variadique vq = new Variadique();
      vq.afficher(3, "J'aime mon prof", 3.14159f, new X());
   }
}
Exécution, implémentation C# Exécution, implémentation C++ Exécution, implémentation Java
int de valeur 3
string de valeur "J'aime mon prof"
float de valeur 3,14159
objet de type effectif inconnu et de valeur "Je suis un X"
int de valeur 3
string de valeur "J'aime mon prof"
float de valeur 3.14159
objet de type effectif inconnu et de valeur "Je suis un X"
int de valeur 3
String de valeur "J'aime mon prof"
float de valeur 3.14159
objet de type effectif inconnu et de valeur "Je suis un X"

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!

Discussion de l'implémentation C#

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é.

using System;
namespace Variadique
{
   class Variadique
   {
      public void Afficher(float f)
      {
         Console.WriteLine("float de valeur {0}", f);
      }
      public void Afficher(int i)
      {
         Console.WriteLine("int de valeur {0}", i);
      }
      public void Afficher(string s)
      {
         Console.WriteLine("string de valeur \"{0}\"", s);
      }

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.

// ...
      public void Afficher(object arg)
      {
         if (arg is float)
         {
            Afficher((float)arg);
         }
         else if (arg is int)
         {
            Afficher((int)arg);
         }
         else if (arg is string)
         {
            Afficher((string)arg);
         }
         else
         {
            Console.WriteLine("objet de type effectif inconnu et de valeur \"{0}\"", arg);
         }
      }

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.

// ...
      public void Afficher(params object[] args)
      {
         foreach (object arg in args)
         {
            Afficher(arg);
         }
      }
   }

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.

// ...
   class X
   {
      public override string ToString()
      {
         return "Je suis un X";
      }
   }

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.

// ...
   class Program
   {
      static void Main(string[] args)
      {
         Variadique vq = new Variadique();
         vq.Afficher(3, "J'aime mon prof", 3.14159f, new X());
      }
   }
}

Discussion de l'implémentation C++

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().

#include <iostream>
#include <string>
using namespace std;

void afficher(float f)
{
   cout << "float de valeur " << f << endl;
}
void afficher(int i)
{
   cout << "int de valeur " << i << endl;
}
void afficher(string s)
{
    cout << "string de valeur \"" << s << "\"" << endl;
}

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.

template <class T>
   void afficher(const T &arg)
   {
       cout << "objet de type effectif inconnu et de valeur \"" << arg << "\"" << endl;
   }

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 :

  • Dans la signature du templates, l'ellipse (le ...) se trouve entre class et le nom Args utilisé pour représenter l'ensemble des types impliqués
  • Dans la signature du paramètre, l.'ellipse se trouve entre Args et le nom args utilisé pour représenter la séquence de paramètres, et
  • Dans la signature de l'appel, l'ellipse suit le nom args pour indiquer au compilateur d'étendre la séquence de paramètres au point d'appel
template <class T, class ... Args>
   void afficher(const T &arg, Args ... args)
   {
      afficher(arg);
      afficher(args...);
   }

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.

class X
{
   friend ostream& operator<<(ostream &os, const X &)
      { return os << "Je suis un X"; }
};

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.

int main()
{
   afficher(3, string("J'aime mon prof"), 3.14159f, X());
}

Discussion de l'implémentation Java

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é.

import java.io.*;

public class Variadique {
   public void afficher(float f) {
      System.out.println("float de valeur " + f);
   }
   public void afficher(int i) {
      System.out.println("int de valeur " + i);
   }
   public void afficher(String s) {
      System.out.println("String de valeur \"" + s + "\"");
   }

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.

// ...
   public void afficher(Object arg) {
      if(arg instanceof Float) {
         afficher((float) arg);
      } else if (arg instanceof Integer) {
         afficher((int) arg);
      } else if (arg instanceof String) {
         afficher((String) arg);
      } else {
         System.out.println("objet de type effectif inconnu et de valeur \"" + arg + "\"");
      }
   }

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.

// ...
   public void afficher(Object ... args) {
      for(Object arg : args) {
         afficher(arg);
      }
   }

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.

// ...
   static class X {
      public String toString() {
         return "Je suis un X";
      }
   }

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.

// ...
   public static void main(String [] args) {
      Variadique vq = new Variadique();
      vq.afficher(3, "J'aime mon prof", 3.14159f, new X());
   }
}

Conclusion

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).


Valid XHTML 1.0 Transitional

CSS Valide !