Ce qui suit est né d'un exemple écrit dans une discussion avec Nicolas Stempak, alors étudiant au CeFTI de l'Université de Sherbrooke.
Il arrive qu'on rencontre des extraits de programmes comme le suivant :
//
// ...
//
const int LARGEUR = 500; // pixels
const int HAUTEUR = 200; // pixels
//
//
//
const int COULEUR = 0x00ff0000; // rouge
//
// ...
//
class Image { /* ... */ };
Image creer_image(); // image surprise!
void dessiner(Image,int); // dessiner une image avec une couleur particulière
//
// ...
//
int main() {
Image img = creer_image();
dessiner(img, LARGEUR); // oups!
}
Ici, il est probable que le code client souhaite dessiner une image d'une certaine largeur, mais que la programmeuse ou le programmeur ait, erronément, appelé la fonction dessiner() qui accepte en paramètre une couleur. C'est à tout le moins que que laissent croire les commentaires dans le programme.
Qu'un programme soit documenté et commenté est une bonne chose, mais les commentaires échappent, règle générale, aux compilateurs, étant en pratique destinés àux gens qui lisent le texte du programme. Pour cette raison, se limiter à des commentaires pour exprimer une intention dans un programme réduit la résilience de ce dernier.
Un truc simple pour améliorer la qualité d'un programme est de rendre l'intention explicite à partir des types utilisés. Ainsi, plutôt que d'utiliser des int et de documenter leur existence en tant que pixels par des commentaires dans un programme de dessin, mieux vaut définir une classe Pixel, de même que des conversions explicites (et non pas implicites!) entre int et Pixel. Ceci force les programmeuses et les programmeurs à expliciter (justement!) leur intention. Par exemple :
class Pixel {
int valeur;
public:
constexpr explicit Pixel(int valeur) : valeur{ valeur } {
}
constexpr explicit operator int() const {
return valeur;
}
};
constexpr Pixel operator "" _pix(unsigned long long valeur) {
return Pixel{ static_cast<int>(valeur) };
}
Notez ici que, grâce à constexpr et aux littéraux maison, utiliser un Pixel est aussi efficace qu'utiliser un int. La classe n'a pas à être complexe ou à offrir une vaste gamme de services pour être utile en tant que telle (en fait, les questions de l'offre de services pour la classe en question et des avantages du caractère fortement typé adjoint aux conversions explicites sont orthogonales).
Avec un type comme Pixel, il devient possible de faire ce qui suit :
//
// ...
//
void f(Pixel) {
}
int main() {
// Pixel p0 = 3; // illégal, constructeur explicite
Pixel p1{ 3 }; // Ok
auto p2 = Pixel{ 3 }; // Ok
// f(3); // illégal, constructeur explicite
f(Pixel{ 3 }); // Ok
// int val0 = p2; // illégal, conversion explicite
int val2 = static_cast<int>(p2); // Ok
f(3_pix); // Ok
auto p3 = 3_pix; // Ok, p3 est de type Pixel
}
On pourrait même, dans une certaine mesure, généraliser la manoeuvre, bien que les gains d'écriture soient limités (cela a le mérite de rendre la manoeuvre homogène, ce qui peut faciliter l'écriture de code générique) :
template <class T>
class ExplicitType {
public:
using value_type = T;
private:
value_type valeur;
public:
constexpr explicit ExplicitType(const value_type &valeur) : valeur{ valeur }{
}
constexpr explicit operator value_type() const {
return valeur;
}
};
struct Pixel : private ExplicitType<int> {
using ExplicitType<value_type>::ExplicitType;
using ExplicitType<value_type>::operator value_type;
};
constexpr Pixel operator "" _pix(unsigned long long valeur) {
return Pixel{ static_cast<int>(valeur) };
}
void f(Pixel) {
}
int main() {
// Pixel p0 = 3; // illégal, constructeur explicite
Pixel p1{ 3 }; // Ok
auto p2 = Pixel{ 3 }; // Ok
// f(3); // illégal, constructeur explicite
f(Pixel{ 3 }); // Ok
// int val0 = p2; // illégal, conversion explicite
int val2 = static_cast<int>(p2); // Ok
f(3_pix); // Ok
auto p3 = 3_pix; // Ok, p3 est de type Pixel
}
Simple et efficace.
Il existe bien sûr d'autres manières d'en arriver à un résultat semblable.
Dans un message qui m'a été adressé en 2017, Onduril (je ne connais pas son vrai nom) propose une approche alternative reposant sur des étiquettes. Les explications qui suivent utilisent d'ailleurs l'exemple qui m'a été envoyé.
L'approche en question repose sur un type générique qui englobe une valeur et une étiquette. La valeur peut être obtenue par voie de conversion explicite, et le constructeur paramétrique est explicite lui aussi. À ce stade, l'idée de pixel (par exemple) n'est pas encore présente. |
|
Pour réifier le concept de pixel, un alias est défini sur la base d'un type étiquette (ici, le type incomplet PixelTag) et du type ExplicitType pour un value_type donné. |
|
Une fois le type Pixel nommé, il est possible de s'en servir par ce nom, comme le montre l'opérateur littéral _pix à droite. |
|
Pour mettre en relief le fait que chaque instanciation du type ExplicitType<T,Tag> pour une paire T, Tag donnée résulte en un type distinct, l'exemple crée un type UID qui repose sur une étiquette (le typ incomplet UIDTag) différente de PixelTag, puis offre deux fonctions prenant respectivement un Pixel et un UID. |
|
Enfin, le programme de test a été enrichi pour montrer que g(UID) ne peut accepter un Pixel en paramètre, même si UID et Pixel sont deux instanciations du type générique ExplicitType<int,Tag> pour des types Tag distincts. Cette approche est utile directement si le type modélisé se limite à une valeur encapsulée, et n'offre pas une vaste gamme de services; il faut l'adapter un peu si l'offre de services est plus large. |
|
Une autre technique alternative repose sur les énumérations fortes, et a été portée à mon attention par Gabriel Dos Reis. Elle va comme suit :
Tout d'abord, définir un type énuméré en choisissant le substrat choisi (le value_type, en pratique), mais sans lui attribuer de valeurs énumérées. |
|
Il demeure possible d'implémenter un littéral maison constexpr, mais en utilisant les parenthèses plutôt que les accolades pour la conversion (pour des raisons grammaticales un peu obscures). |
|
... et c'est tout! En pratique, le type Pixel est un « types fort », avec lequel les conversions « de » et « vers » le substrat doivent être faites de manière explicite. |
|
C'est simple et limité à des substrats entiers, mais ça fonctionne et ça couvre tout de même plusieurs cas d'utilisation.