DSEL et C++: définition et illustration avec std::initializer_list

——————————————-
Cet article est un miroir de l’article présent sur mon blog : DSEL et C++: Définition et illustration avec std::initializer_list
——————————————-

Au cours du mois dernier, j’ai eu la chance de pouvoir traduire le premier article d’une longue série consacrée aux Domain-Specific Embedded Languages (DSEL) en C++ sur un site qui me tient beaucoup à coeur : Developpez.com. La série de base a été publiée il y a un plus de deux ans par Eric Niebler sur le blog C++Next. Et au début de ce mois, j’ai été ravi de voir ma petite contribution publiée à cette adresse : Le C++ expressif n°1 : Introduction, premier article de la série C++ expressif avec Boost.Proto.

Cet évènement est alors l’occasion pour moi de vous parler des DSEL car c’est un sujet qui mérite l’attention et parce que je souhaite vraiment que de nouveaux DSEL se développent. Dans la deuxième partie de cet article, nous construirons notre propre petit DSEL pour s’assurer d’avoir bien saisi le concept.

« Domain-Specific Embedded Language »

Avoir traduit l’article est une opportunité pour moi d’exprimer avec mes propres mots et mes propres questions sur: qu’est-ce qu’un DSEL ? Je n’ai rien contre l’article d’origine, au contraire sinon je ne l’aurai pas traduit, mais celui-ci est long et déjà destiné à un public averti. Je souhaiterai juste, à travers cette partie, reformuler et synthétiser ce concept.

Concrètement, comment faut-il comprendre cette expression ?

  • Domain-Specific : c’est spécialisé dans un domaine.
  • Embedded : c’est intégré, il n’y a aucun rapport avec l’Embedded en informatique.
  • Language : c’est un langage informatique.

Un DSEL c’est donc un petit langage intégré à l’intérieur même d’un langage hôte et on va utiliser ce petit langage car sa syntaxe est adaptée à un domaine en particulier.

Il ne faut absolument pas le confondre avec un Domain-Specific Language, un exemple de DSL que vous connaissez sûrement est le langage SQL, c’est un langage à part entière, il n’est pas intégré dans un langage hôte et il est spécialisé pour travailler avec les bases de données.

Le point le plus important à propos d’un DSEL c’est que pour en créer un, on n’utilise QUE les spécificités et la syntaxe d’un langage de base. Autrement dit, notre petit langage intégré DOIT pouvoir être compilé par le compilateur du langage sans aucune autre extension dans le processus.

Mais alors, comment fait-on pour créer un langage si on ne peut pas modifier un peu sa syntaxe ? Tout simplement grâce à deux concepts très puissants du C++ : la métaprogrammation avec des templates et la surcharge d’opérateur.

Bien sûr comme toute chose en matière de programmation, il y a toujours des développeurs pour pousser la conception au plus haut niveau afin de permettre à l’utilisateur de ne se soucier que de son problème. Et cela existe aussi pour la conception de DSEL avec notamment Boost.Proto mais cela je vous laisse le découvrir à travers la série : C++ expressif avec Boost.Proto. ;)

Pourquoi utiliser un DSEL ?

Pour rendre le code plus lisible quand on traite un domaine en particulier évidemment ! Nous allons voir ça tout de suite avec un exemple.

Cas pratique : notre propre DSEL

Trêve de bavardage, l’aspect théorique est terminé, on va enfin pouvoir s’amuser !

Afin de créer notre propre petit DSEL, on va s’imaginer une problématique. On souhaiterait travailler avec des ensembles en mathématiques. Tout ce qu’on souhaite pour les besoins de ce billet, c’est de créer notre ensemble et de pouvoir faire l’union d’ensembles comme si on faisait des maths ! Malheureusement la syntaxe C++ n’est pas ce qu’il y a de plus adaptée.

Pour créer notre manipulation d’ensemble, on va utiliser trois points clés :

  • std::initializer_list
  • la surcharge d’opérateur
  • un petit define

A la toute fin de ce billet, notre DSEL ressemblera à ça :

int main(int argc, char* argv[])
{
  Ensemble E1 {42, 1337};

  Ensemble E2 = Ensemble {1, 3, 5}   U    Ensemble {2, 4, 6};
   
  Ensemble Final = E1   U   E2;
   
  return 0;
}

C’est plutôt pas mal non ? ;)

Un peu de C++11

On va faire un premier jet et on va voir comment on peut améliorer ça. On ne va pas non plus réinventer la roue et on va directement utiliser std::set. Pour rappel, ce container assure que chaque élément est unique et il trie automatiquement de façon croissante.

#include
#include

int main(int argc, char* argv[])
{
  std::set set;
  set.insert(1);
  set.insert(2);
  set.insert(3);
  set.insert(4);
  set.insert(5);
  set.insert(6);
   
  return 0;
}

Bon c’est pas terrible mais la bonne nouvelle c’est qu’on peut améliorer ça facilement ! On va l’améliorer grâce à une nouveauté de la nouvelle norme C+11 : les listes d’initialiseurs et l’initialisation uniforme. Attention : vérifiez que votre compilateur est à jour (g++ 4.7 dans mon cas) pour pouvoir compiler les prochains codes de ce billet et n’oubliez pas l’argument -std=c++11.

Attention : il ne faut pas confondre la liste d’initialiseurs {1, 2, 3} et la liste d’initialisations d’un constructeur MaClasse() : _membre1(0), _membre2(12).

#include
#include

int main(int argc, char* argv[])
{
  std::set set1 {1, 2, 3, 4, 5, 6};
  // équivalent à
  std::set set2 = {1, 2, 3, 4, 5, 6};
  // équivalent à
  std::set set3 ({1, 2, 3, 4, 5, 6});
 
  return 0;
}

Pour initialiser notre ensemble, c’est déjà bien mieux ! Mais bon, on ne va pas dire qu’on a déjà créé notre DSEL car on n’a encore rien codé du tout. Tout ce qu’on a fait, c’est reprendre ce que C++11 nous proposait déjà.

La structure « Ensemble »

A présent, nous allons ajouter la possibilité de récupérer l’union de deux ensembles. Pour satisfaire ce besoin, on va créer une structure afin de wrapper un std::set.

Je vais construire cette structure pas à pas avec des explications :

struct Ensemble
{
  std::set _set;
};

Dans la partie précédente, j’ai oublié de vous mentionner quelque chose. La jolie syntaxe disponible depuis C++11 pour initialiser un container {x, y, z, …}, on peut aussi l’utiliser nous-même pour nos objets en tant qu’argument de constructeur ou de fonction membre ! Pour cela, on utilise std::initializer_list qui se comporte comme un objet proxy récupérant l’initialisation sous forme de tableaux {} et renvoyant une liste. On l’ajoute à notre constructeur.

struct Ensemble
{
  Ensemble(std::initializer_list list) // pour permetre les initialisations comme un tableau {1, 2, 3, …}
    : _set(list) // on copie le contenu de notre liste dans notre set
  {

  }
 
  std::set _set;
};

Grâce à notre constructeur, on va pouvoir créer des objets de cette manière :

Ensemble E1 {1, 2, 3};
Ensemble E1 = {1, 2, 3};
Ensemble E1 ({1, 2, 3});

Finalement, il ne nous manque plus qu’à coder "l’union de deux ensembles". Pour cela, nous allons simplement surcharger l’opérateur + pour renvoyer un nouvel ensemble, comme si l’union était une opération binaire (c’est-à-dire avec deux opérandes) de type a = b + c. Encore une fois, la bibliothèque standard est bien fournie et propose la fonction std::set_union qui à partir d’itérateurs d’entrée et de sortie va effectuer l’union de deux containers.

struct Ensemble
{
  Ensemble(std::initializer_list list) // pour permetre les initialisations comme un tableau {1, 2, 3, …}
    : _set(list) // on copie le contenu de notre liste dans notre set
  {

  }
 
  Ensemble(std::set set)
    : _set(set)
  {

  }
 
  Ensemble Union(const Ensemble& ensemble)
  {
    std::set result;

    // union des deux ensembles
    std::set_union(
      _set.begin(),
      _set.end(),
      ensemble._set.begin(),
      ensemble._set.end(),
      std::inserter(result, result.begin()) // itérateur d'insertion
    );
   
    // il n'y a pas besoin de trier ni de vérifier
    // que chaque élément est unique, std::set le fait automatiquement !

    return Ensemble(result);
  }

  Ensemble operator+(const Ensemble& e)
  {
    return Union(e);
  }
 
  std::set _set;
};

Vous avez peut-être remarqué que j’utilise un itérateur d’insertion : std::inserter. Pour la simple et bonne raison qu’on travaille avec des std::set et que les valeurs ce container sont immuables après initialisation. Si on avait travaillé avec des std::vector, nous aurions pu remplacer std::inserter(result, result.begin()) par result.begin().

Petit point sémantique: Ensemble est une structure à sémantique de valeur, n’oubliez donc pas de coder sa forme canonique orthodoxe de Coplien. Je ne l’affiche pas par gain d’espace mais pensez-y !

Last but not least, on va se permettre une petite fantaisie. En tête de notre fichier on va placer cela pour imiter la notation U comme l’union en mathématiques. Attention: ne nommez pas une variable seulement "U" après ça, elle sera remplacée par un + à la compilation.

#define U +

Ainsi grâce à nos petites manoeuvres, ces codes là sont équivalents :

<code class="sourceCode cpp"Ensemble E1 = Ensemble ({1, 2, 3}) + Ensemble({4, 5, 6});

Ensemble E1 = Ensemble {1, 2, 3}   U   Ensemble {4, 5, 6};

Nous garderons la deuxième syntaxe car elle est bien plus proche de ce qu’on a en mathématiques.

Nous pouvons être fier puisque nous venons d’écrire notre tout petit DSEL pour manipuler des ensembles !

Code récapitulatif :

#include
#include
#include

#define U +

struct Ensemble
{
  Ensemble(std::initializer_list list) // pour permetre les initialisations comme un tableau {1, 2, 3, …}
    : _set(list) // on copie le contenu de notre liste dans notre set
  {

  }
 
  Ensemble(std::set set)
    : _set(set)
  {

  }
 
  Ensemble Union(const Ensemble&amp; ensemble)
  {
    std::set result;

    // union des deux ensembles
    std::set_union(
      _set.begin(),
      _set.end(),
      ensemble._set.begin(),
      ensemble._set.end(),
      std::inserter(result, result.begin()) // itérateur d'insertion
    );
   
    // il n'y a pas besoin de trier ni de vérifier
    // que chaque élément est unique, std::set le fait automatiquement !

    return Ensemble(result);
  }

  Ensemble operator+(const Ensemble&amp; e)
  {
    return Union(e);
  }
 
  std::set _set;
};

int main(int argc, char* argv[])
{
  Ensemble E1 {42, 1337};

  Ensemble E2 = Ensemble {1, 3, 5} U Ensemble {2, 4, 6};
  // E2 {1, 2, 3, 4, 5, 6}
   
  Ensemble Final = E1   U   E2;
  // Final {1, 2, 3, 4, 5, 6, 42, 1337}
   
  return 0;
}

Conclusion

Cet article touche à sa fin, nous avons créer notre petit DSEL juste avec la bibliothèque standard, ça n’était pas très compliqué mais au final, quand on travaille sur des ensembles c’est bien plus lisible comme ça non ? Si vous voulez le continuer un petit peu, la lib standard propose set_intersection renvoyant l’intersection de deux containers. Après tout avec l’exemple au dessus, ça ne devrait pas être compliqué et ça serait plutôt sympathique d’écrire quelque chose en remplaçant le symbole de l’intersection ∩ par un n :

Ensemble Final = Ensemble {1, 2, 3}  n  Ensemble {2, 3, 4};

Tout de même, j’avoue que je n’ai pas choisi le sujet le plus difficile, la syntaxe du C++ avec les initialisations entre accolades se prétaient déjà bien au jeu. Mais il existe des utilisations bien plus impressionnantes, dans cet article par exemple Expressive C++: A Lambda Library in 30 Lines, en utilisant Boost.Proto, on arrive à créer une petite library pour écrire des fonctions lambdas !

Si vous voulez approfondir le sujet, vous pouvez toujours lire la série C++ expressif avec Boost.Proto. N’hésitez pas à me faire part de vos améliorations/questions/commentaires !

Bienvenue sur mon blog sur Developpez.com !

Bonjour à tous et bienvenue sur mon blog sur Developpez.com,

Je possède déjà un blog à mon nom disponible à cette adresse : http://timothee-bernard.fr. Je m’occuperais donc principalement de copier mes articles en rapport avec l’informatique ici pour en faire profiter Developpez.com et j’espère que ça vous plaira. :)

Si vous voulez en savoir plus sur moi vous pouvez visiter cette page : Ouverture de mon blog éponyme.

Concrètement, de quoi je vais parler ?

  • Je parlerai de C++ ou même de langage de programmation ou même encore de domaines pas très populaires ! Dans tous les cas, j’essaierai de faire des articles beaucoup plus poussées que ce qu’on a l’habitude de voir qui abordent des thèmes peu communs.
  • Je vais aussi essayer d’aborder des thèmes qui peuvent paraitre compliqués dans un aspect plus « vulgaire » afin d’être facilement compréhensible. Un exemple d’article qui me vient à l’esprit est par exemple : « Comment fonctionne un débuggueur ? » « Comment vulgairement créer un système d’exploitation ? ».
  • Je proposerai des ressources, des articles ou des cours que j’ai déjà lu pour en faire une petite synthèse et vous sélectionner le meilleur d’Internet. :o)
  • Des réflexions, des pensées, des avis, des expériences, des opinions sur des sujets, toujours dans le cadre de l’informatique.
  • Et d’autres choses à venir !
  • Bonne lecture à tous !