Logo Spiria

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

31 janvier 2019.

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.

Quand je regarde du code C++ classique, je vois beaucoup d’utilisations de types de base : int, float, double, std::wstring, const char * -- 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.

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.

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.

La situation actuelle

C++ possède l’instruction typedef, 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 Celsius et Fahrenheit à partir d’un double avec un typedef car rien n’empêche d’additionner des Celsius avec des Fahrenheit.

En apparence, cette propriété de typedef 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, typedef 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 typedef, par exemple ListOfPeople, aide à comprendre le code, mais n’empêche pas de lui attribuer une liste de noms de fichiers par erreur.

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’ints, de doubles et de typedef, parce qu’ils sont faciles à créer.

Création facile de types

Ce dont nous avons besoin est quelque chose d’aussi simple qu’un typedef qui puisse vraiment créer un nouveau type. Appelons-le typedecl. 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 typedecl devrait pouvoir faire :

  1. Créer un nouveau type.
  2. Autoriser une déclaration facile des valeurs littérales.
  3. Inclure automatiquement les fonctions internes.
  4. Pouvoir inclure facilement des fonctionnalités externes.

1. Créer un nouveau type

Créer un nouveau type est facile. Il suffit d’en faire la définition de typedecl dans le langage C++. Avec un nouveau type, nous évitons les affectations accidentelles et permettons la surcharge des fonctions. En prenant notre exemple de Celsius et Fahrenheit, voici deux déclarations de fonctions qui ne pourraient pas être écrites côte à côte si ces types étaient un typedef :

Celsius convert(Fahrenheit);
Fahrenheit convert(Celsius);

Bien que n’importe qui pourrait suggérer des noms de fonctions pour permettre à cela de fonctionner avec typedef, 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.

2. Autoriser une déclaration facile des variables littérales

Une déclaration facile des variables littérales est importante pour la facilité d’utilisation. Sans cela, ces typedecl ne seraient pas utilisés. Un peu de la façon dont un littéral numérique sera automatiquement et silencieusement typé comme un int, un long ou un double s’il correspond aux limites du type, typedecl devrait offrir le même comportement.

3. Inclure les fonctions internes

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 (int, double, …) nous ne voulons pas avoir à déclarer toutes les opérations possibles entre deux variables. Si c’est fastidieux, le typedecl 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 typdecl. Par exemple, un typedecl basé sur un std::string devrait inclure ses fonctions membres, avec toutes les instances des paramètres std::string remplacées par le nouveau type.

4. Inclure des fonctions externes

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é, typedecl, avec un modificateur de clonage. Cela permettrait le clonage d’une ou plusieurs fonctions. Par exemple :

typedecl Celsius clone std::abs;
typedecl Celsius clone { std::abs; std::pow, ... }

Idéalement, il devrait être facile de cloner aussi un espace de noms au complet :

typedecl Celsius clone namespace std;

Malheureusement, dans de nombreux cas, cette approche est trop large et aveugle. Idéalement, il faudrait ajouter l’équivalent d’un namespace 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++ :

namespace std
{
   namespace semantic trig
   {
      double cos(double);
      double sin(double);
   }

   namespace semantic io
   {
      ostream& operator << (ostream&, double);
      // ...
   }
}

Avec cette fonctionnalité, la fonction cos() est toujours accessible directement dans le namespace std. L’utilisation du namespace trig sémantique serait autorisé, mais facultatif. Le clonage de toutes les fonctions trigonométriques deviendrait simplement :

typedecl Celsius clone namespace std::trig;

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 :

typedecl Celsius clone double std::pow(double, double) -> Celsius std::pow(Celsius, double);

Les bénéfices

Maintenant, je vais montrer quelques exemples d’améliorations du code qui peuvent être apportées avec typedecl. Tout d’abord, cela peut contribuer à moins d’erreurs de codage lorsque les arguments d’une fonction sont mal placés :

// 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);
}

Deuxièmement, il permet de spécialiser la surcharge ou les templates 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 typedef. Avec typedef, vous devez savoir quel est le type sous-jacent pour savoir si une surcharge ou une instanciation de template est vraiment différente. Si vous utilisez le typedef 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 std::variant. 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 typedecl, le fait de créer différents types fait disparaître ce problème.

En conclusion

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 int, double, std::string, std::map, etc.. Nous pourrions programmer avec des types ayant du sens qui offriraient plus de sécurité car les créer serait suffisamment simple.