C++ hypothétique : création facile de types

Dans cet essai, nous allons explorer une extension imaginaire du langage C++ qui pourrait simplifier la programmation, aider à détecter plus d’erreurs de façon automatique et rendre le code plus clair.
Coin des développeurs
Équipe Dev Spiria
2019-01-28 11:27
5 min. de lecture
<p>Quand je regarde du code C++ classique, je vois beaucoup d’utilisations de types de base : <code>int</code>, <code>float</code>, <code>double</code>, <code>std::wstring</code>, <code>const char *</code> -- même s’ils sont utilisés dans chaque cas à des fins extrêmement différentes. Ils peuvent représenter une largeur ou une hauteur, une température, un seuil ou une limite.</p><p>La raison pour laquelle les programmeurs optent pour ces types simples est facile à expliquer : ils sont intégrés, ils ont beaucoup de fonctions de support, et leur comportement est bien connu. Mais il y a une autre raison derrière leur omniprésence : les langages de programmation rendent difficile la création d’un nouveau type avec suffisamment de fonctionnalités.</p><p>Et si ce n’était pas le cas ? Ce que je souhaiterais avoir en C++ est un moyen facile de créer un nouveau type à partir d’un type existant.</p><h2>La situation actuelle</h2><p>C++ possède l’instruction <code>typedef</code>, mais cela crée simplement un synonyme pour un type existant, et les deux sont interchangeables. Vous ne pouvez pas sans risque créer des types <code>Celsius</code> et <code>Fahrenheit</code> à partir d’un <code>double</code> avec un <code>typedef</code> car rien n’empêche d’additionner des <code>Celsius</code> avec des <code>Fahrenheit</code>.</p><p>En apparence, cette propriété de <code>typedef</code> semble utile. Si nous n’avons besoin que d’une version abrégée pour une déclaration de type long ou d’un nom sémantique pour un type, <code>typedef</code> est la solution. Mais est-ce vraiment le cas ? L’utilisation d’une version purement abrégée permet d’économiser des frappes, mais n’aide pas à la compréhension du code. D’autre part, donner un nom sémantique au <code>typedef</code>, par exemple <code>ListOfPeople</code>, aide à comprendre le code, mais n’empêche pas de lui attribuer une liste de noms de fichiers par erreur.</p><p>La manière courante d’éviter de telles erreurs de frappe est d’envelopper le type dans une classe. L’inconvénient, c’est qu’il faut redéclarer chaque fonction membre, ou recréer beaucoup d’opérateurs. Le fait que cette solution existe et qu’elle n’est que rarement utilisée devrait nous en dire long sur les embûches. Le code typique demeure parsemé d’<code>ints</code>, de <code>doubles</code> et de <code>typedef</code>, parce qu’ils sont faciles à créer.</p><h2>Création facile de types</h2><p>Ce dont nous avons besoin est quelque chose d’aussi simple qu’un <code>typedef</code> qui puisse vraiment créer un nouveau type. Appelons-le <code>typedecl</code>. Idéalement, il serait si simple à utiliser que les programmeurs l’utiliseraient par défaut. Il faut éliminer le plus possible d’obstacles à leur utilisation. Voici ce qu’un <code>typedecl</code> devrait pouvoir faire :</p><ol> <li>Créer un nouveau type.</li> <li>Autoriser une déclaration facile des valeurs littérales.</li> <li>Inclure automatiquement les fonctions internes.</li> <li>Pouvoir inclure facilement des fonctionnalités externes.</li></ol><h3>1. Créer un nouveau type</h3><p>Créer un nouveau type est facile. Il suffit d’en faire la définition de <code>typedecl</code> dans le langage C++. Avec un nouveau type, nous évitons les affectations accidentelles et permettons la surcharge des fonctions. En prenant notre exemple de <code>Celsius</code> et <code>Fahrenheit</code>, voici deux déclarations de fonctions qui ne pourraient pas être écrites côte à côte si ces types étaient un <code>typedef</code> :</p><pre><code>Celsius convert(Fahrenheit);Fahrenheit convert(Celsius);</code></pre><p>Bien que n’importe qui pourrait suggérer des noms de fonctions pour permettre à cela de fonctionner avec <code>typedef</code>, le fait d’être obligé de trouver un tel schéma et, plus important encore, d’avoir besoin de s’en inquiéter en premier lieu, souligne le problème de ne pas avoir un type unique pour chacun.</p><h3>2. Autoriser une déclaration facile des variables littérales</h3><p>Une déclaration facile des variables littérales est importante pour la facilité d’utilisation. Sans cela, ces <code>typedecl</code> ne seraient pas utilisés. Un peu de la façon dont un littéral numérique sera automatiquement et silencieusement typé comme un <code>int</code>, un <code>long</code> ou un <code>double</code> s’il correspond aux limites du type, <code>typedecl</code> devrait offrir le même comportement.</p><h3>3. Inclure les fonctions internes</h3><p>L’inclusion automatique des fonctions internes est encore une fois une réponse à notre but de rendre l’utilisation simple. Par exemple, avec les types numériques (<code>int</code>, <code>double</code>, …) nous ne voulons pas avoir à déclarer toutes les opérations possibles entre deux variables. Si c’est fastidieux, le <code>typedecl</code> ne sera pas utilisé, tout comme envelopper un entier dans une classe est rarement utilisé. Il devrait aussi en aller de même pour les types plus complexes qui pourraient servir de base à un <code>typdecl</code>. Par exemple, un <code>typedecl</code> basé sur un <code>std::string</code> devrait inclure ses fonctions membres, avec toutes les instances des paramètres <code>std::string</code> remplacées par le nouveau type.</p><h3>4. Inclure des fonctions externes</h3><p>La partie la plus difficile est la dernière : permettre d’inclure des fonctions externes au type. Une fois de plus, la facilité d’utilisation pour le programmeur aura une influence directe sur la fréquence d’utilisation. Il devrait être facile de cloner une fonction existante pour le nouveau type. Idéalement, il devrait être facile de cloner tout un groupe de fonctions. La syntaxe que je suggère est de réutiliser le même mot-clé, <code>typedecl</code>, avec un modificateur de clonage. Cela permettrait le clonage d’une ou plusieurs fonctions. Par exemple :</p><pre><code>typedecl Celsius clone std::abs;typedecl Celsius clone { std::abs; std::pow, ... }</code></pre><p>Idéalement, il devrait être facile de cloner aussi un espace de noms au complet :</p><pre><code>typedecl Celsius clone namespace std;</code></pre><p>Malheureusement, dans de nombreux cas, cette approche est trop large et aveugle. Idéalement, il faudrait ajouter l’équivalent d’un <code>namespace</code> en C++, sans création d’un identifiant supplémentaire lors de la programmation, mais en créant simplement un groupement sémantique. Par exemple, toutes les fonctions trigonométriques pourraient être regroupées sous une sémantique et toutes les fonctions d’entrées-sorties sous une autre. Voici à quoi pourrait ressembler cet aspect hypothétique du C++ :</p><pre><code>namespace std{ namespace semantic trig { double cos(double); double sin(double); } namespace semantic io { ostream&amp; operator &lt;&lt; (ostream&amp;, double); // ... }}</code></pre><p>Avec cette fonctionnalité, la fonction <code>cos()</code> est toujours accessible directement dans le <code>namespace</code> <code>std</code>. L’utilisation du <code>namespace </code><code>trig</code> sémantique serait autorisé, mais facultatif. Le clonage de toutes les fonctions trigonométriques deviendrait simplement :</p><pre><code>typedecl Celsius clone namespace std::trig;</code></pre><p>Dans certains cas, il peut être utile de ne modifier que certains paramètres d’une fonction. Pour cela, nous pourrions emprunter la syntaxe d’un guide de déduction pour donner au compilateur une carte sur la façon dont la conversion automatique doit être faite. Par exemple :</p><pre><code>typedecl Celsius clone double std::pow(double, double) -&gt; Celsius std::pow(Celsius, double);</code></pre><h2>Les bénéfices</h2><p>Maintenant, je vais montrer quelques exemples d’améliorations du code qui peuvent être apportées avec <code>typedecl</code>. Tout d’abord, cela peut contribuer à moins d’erreurs de codage lorsque les arguments d’une fonction sont mal placés :</p><pre><code>// Dans notre monde…void foo(){ int width = get_width(); int height = get_height(); bool stroked = should_stroke(); bool filled = should_fill(); // Cet appel est-il correct ? draw_rect(width, height, stroked, filled);}// Dans le C++ hypothétique...void foo(){ Width width = get_width(); Height height = get_height(); Stroked stroked = should_stroke(); Filled filled = should_fill(); // L’ordre des arguments est nécessairement correct. draw_rect(width, height, stroked, filled);}</code></pre><p>Deuxièmement, il permet de spécialiser la surcharge ou les <code>templates</code> en fonction de la sémantique d’un type plutôt que de son type purement mécanique. C’est beaucoup mieux qu’avec le <code>typedef</code>. Avec <code>typedef</code>, vous devez savoir quel est le type sous-jacent pour savoir si une surcharge ou une instanciation de <i>template</i> est vraiment différente. Si vous utilisez le <code>typedef</code> d’une librairie quelconque, vous devez l’envelopper dans une classe, avec tous les inconvénients de l’interfaçage. Par exemple, prenez le type <code>std::variant</code>. Il permet d’accéder à ses éléments par leur type, mais si deux éléments ont le même type, il y a ambiguïté. Avec <code>typedecl</code>, le fait de créer différents types fait disparaître ce problème.</p><h2>En conclusion</h2><p>Avec ces modifications apportées à C++, nous pourrions finalement nous débarrasser de nombreuses utilisations par défaut et paresse de types purement mécaniques. Il n’y aurait plus aucune raison d’utiliser dans le code de simples <code>int</code>, <code>double</code>, <code>std::string</code>, <code>std::map</code>, etc.. Nous pourrions programmer avec des types ayant du sens qui offriraient plus de sécurité car les créer serait suffisamment simple.</p>

Travaillons ensemble