Logo Spiria

C++ hypothétique : des qualificateurs extensibles

14 février 2019.

Dans un programme C++, les données proviennent d’une source, sont modifiées plusieurs fois et finissent par être complètement différentes. Au cours du traitement, les données peuvent changer de type au fur et à mesure qu’elles sont traitées, ce qui reflète les changements subits. En tant que programmeurs, nous concevons un ensemble de types qui reflète la signification sémantique des données, leur structure et leur état actuel. Idéalement, le système de types de C++ devrait nous donner tout le vocabulaire dont nous avons besoin.

C++ a accès à un riche ensemble d’outils pour concevoir des types et créer ce vocabulaire. Son système de types est ouvert et permet au programmeur de définir de nouveaux types selon ses besoins. Pourtant, il y a un aspect qui demeure fermé : les qualificateurs de type.

C++ nous offre deux qualificateurs de type : const et volatile(1). Ils permettent de donner un sens supplémentaire à un type existant, sans avoir à créer un type séparé à la main. Le qualificateur const est bien sûr le plus utilisé. S’il n’existait pas, il serait fastidieux de devoir reproduire son comportement pour chaque type que nous créons. Sans lui, C++ serait beaucoup moins expressif et il serait plus difficile de l’utiliser pour programmer. Et pourtant, ce pouvoir expressif se limite à ces deux qualificateurs. Il y a un décalage entre leur utilité et notre incapacité à en créer de nouveaux.

Qualificateurs définis par l’utilisateur

Ce que j’aimerais voir dans C++ est la possibilité pour le programmeur de définir ses propres qualificateurs. Je vais vous donner quelques exemples de ce qui peut être réalisé avec des qualificateurs définis par l’utilisateur, mais décrivons d’abord comment ils fonctionneraient. C’est très simple, vraiment. Ils fonctionneraient exactement comme const et volatile. C’est à dire :

  • La capacité de qualifier un type ou une fonction membre.
  • Une équivalence unidirectionnelle automatique et optionnelle pourrait être déclarée, similaire à la manière dont on passe un pointeur non-const à une fonction qui prend un pointeur const.
  • La possibilité d’une conversion forcée entre le type qualifié et le type non qualifié à l’aide d’un const_cast(2).

Je ne veux pas me concentrer sur les détails d’une syntaxe fictive. Je crois que ce n’est pas très important et tout le monde pourrait proposer quelque chose. Je vais juste montrer une possibilité en utilisant un nouveau mot-clé typequal :

typequal NewQualifier;
typequal NewQualifier auto qualified;
typequal NewQualifier auto non-qualified;
typequal NewQualifier invalid;
typequal NewQualifier invalid auto qualified;

Chacun de ces exemples créerait un qualificateur nommé NewQualifier. La variante « auto qualified » indique que le qualificateur peut être ajouté silencieusement à un type quand il est affecté à une variable, comme fonctionne le qualificateur const. La variante « auto non-qualified » permet la conversion automatique inverse. Qu’en est-il de la variante « invalid » ? Celle-ci déclare que les données avec le qualificateur ne sont pas accessibles. Comme vous le verrez, c’est une fonctionnalité utile à avoir.

Les bénéfices

Maintenant, je voudrais vous convaincre de l’utilité de cette fonctionnalité. Essayons de résoudre quelques problèmes qui causent de vrais bogues dans de vrais programmes.

1. Pointeurs Null

Commençons par les pointeurs null. Le déréférencement des pointeurs null est une source majeure de bogues. Avoir des pointeurs « nullables » est souvent décrié comme une erreur majeure dans la conception du langage. Mais le vrai problème n’est pas en soi le pointeur null, mais en fait que le langage ne nous empêche pas d’utiliser un pointeur null. Corrigeons ça :

typequal maybe invalid;

Voilà. Désormais, chaque fonction qui produit un pointeur doit produire un pointeur avec le type sous-jacent agrémenté du qualificateur maybe. Comme c’est un qualificateur de marquage invalide, le langage ne nous permettra pas d’utiliser les données. Une fois que vous avez testé pour null, vous pouvez faire le « const_cast » pour supprimer le qualificateur maybe. Bien sûr, il serait bien plus pratique si le langage supportait nativement un tel qualificateur, de sorte que les fonctions intégrées prendraient et donneraient des pointeurs déjà qualifiés.

2. Données non-validées

Des qualificateurs similaires peuvent être utilisés pour décrire différents états de données. Deux exemples qui reviennent souvent dans le code seraient :

typequal invalidated invalid auto qualified;
typequal tainted invalid auto qualified;

Le premier pourrait être utilisé pour marquer les données quand elles n’ont pas encore été validées en regard d’une contrainte souhaitée. Souvent, un groupe de fonctions posera de telles contraintes sur ses entrées. En s’assurant par design que le point d’entrée prenne des données invalidated et les fonctions internes qui prennent des données non qualifiées, nous pouvons nous assurer que les fonctions internes ne peuvent être appelées sans que les données soient validées.

La seconde est une idée empruntée à Perl : que les données tainted ne sont pas fiables. Le marqueur est similaire au invalidated, mais au lieu de simplement ne pas suivre certaines contraintes, les données doivent être traitées entièrement avec suspicion. En Perl, ces données tainted proviennent du Web, de courriels et d’autres sources non fiables. Des précautions supplémentaires doivent donc être prises lors de la validation de ces données.

3. État des données

Bien sûr, afin de refléter la progression d’un algorithme, un tel système de validation peut être étendu pour prendre en charge de multiples états. Il peut aussi refléter différents types de validation. Voici quelques idées :

// Les données sont ordonnées.
typequal sorted;

// Ordonne un vecteur et retourne ce même vecteur avec le qualificateur.
sorted vector<int>& sort(vector<int>& unsorted_vector);

// Fait une recherche binaire sur un vecteur seulement s'il a été ordonné.
bool find_in_data(const sorted vector<int>& sorted, int value);

// Les données sont partagées entre des threads.
// On créerait un verrou (Lock) sur un mutex prenant
// ces données en argument et qui enlèverait le qualificateur.
typequal shared invalid;

// Quel système de coordonnées est utilisé en 3D.
// Évite d'utiliser des points en coordonnées "locales"
// dans un algorithme nécessitant des points "monde".
typequal local;
typequal world;
typequal view;
typequal screen;

// Lors de l'application de la bonne matrice, les points
// sont qualifiés avec le bon qualificateur de coordonnées.
world vector& apply_world_matrix(
	local vector&, const local world matrix&);

Conclusion

Mon objectif était de vous faire voir l’intérêt qu’il y aurait à ajouter des qualificateurs définis par l’utilisateur dans le langage C++. Comme je l’ai montré, avec les qualificateurs définis par l’utilisateur, nous ouvrons la porte à de nombreuses possibilités ayant de grands avantages :

  • Être plus expressif avec les types de données existants.
  • Présenter clairement au lecteur du code quel est l’état des données.
  • Suivre la progression de l’évolution des données.
  • Permettre au compilateur de faire respecter diverses contraintes.
  • Éviter les bogues liés à la transmission de données incorrectes aux fonctions.


 


(1) Il y a aussi les qualificateurs restricted, register et auto, mais ils ne fonctionnent pas comme const et volatile.

(2) Bien qu’il serait plus élégant de le renommer en qualifier_cast.