Ce qui suit est subtil et un peu technique. On y utilise des techniques de programmation générique et des templates de même que des pointeurs de fonctions . Vous voilà averti(e).
Cet article deviendra désuet avec l'avènement probable des concepts à partir de C++ 17
Il peut arriver que nous souhaitions écrire des programmes utilisant des types devant supporter un certain nombre d'opérations, et qu'il soit désirable à la compilation de vérifier le support des opérations en question pour ces types, sans toutefois qu'on veuille nécessairement instancier ces types.
Par exemple, présumons qu'un programme ait besoin que les types qu'il manipule soient comparables, au sens où on doit pouvoir leur appliquer certains opérateurs relationnels (nous vérifierons le support de < et de <= ici, mais la liste pourrait être aussi longue ou aussi courte que nécessaire).
Rédigeons un tel test. Imaginons la classe Comparable, un template applicable à un type T :
template <typename T>
class Comparable
{
//
// contraintes(T,T) doit être une méthode de classe
// (static), pour des raisons qui deviennent claires
// dans le constructeur de Comparable<T> (plus bas)
//
static void contraintes(T a, T b)
{
//
// solliciter les opérations dont on veut tester le
// support pour le type T. Cette fonction ne sera
// pas créée tant que le compilateur ne lui reconnaîtra
// pas la chance de servir
//
(void)(a < b); // operator<(T,T) existe-t-il?
(void)(a <= b); // operator<=(T,T) existe-t-il?
}
public:
Comparable()
{
//
// prendre un pointeur sur la méthode contraintes(T,T);
// on n'appelle pas la méthode, mais en prenant son
// adresse on force le compilateur à générer son code
// pour des paramètres de type T
//
void (*p)(T,T) = contraintes;
}
};
Pourquoi cet artifice technique? Pour que des erreurs à la compilation soient détectées si les opérations tentées dans la méthode contraintes(T,T) ne sont pas supportées sur le type T.
Par exemple, instancier un Comparable<int> réussira parce que les opérations < et <= existent pour le type int, instancier un Comparable<X> pour une classe X quelconque ne supportant pas < ou <= échouera dès la compilation.
Le programme principal montre bien la force de la technique :
La beauté ici est que instancier un Comparable<T> ne crée aucun T, mais valide quand même à la compilation une série arbitrairement longue et complexe d'opérations sur T.
#include "Comparable.h" // classe Comparable ci-dessus
#include <string> // toute std::string supporte < et <=
// La classe Incomparable n'est pas Comparable au sens entendu
// ici: elle ne supporte pas les opérateurs < et <=
class Incomparable { };
// La classe TiteSomme est comparable au sens entendu ici...
class TiteSomme
{
const int x_, y_;
int x() const
{ return x_; }
int y() const
{ return y_; }
public:
TiteSomme(const int x, const int y)
: x_(x), y_(y)
{
}
int valeur() const
{ return x_ + y_; }
bool operator< (const TiteSomme &ts) const
{ return valeur() < ts.valeur(); }
bool operator<= (const TiteSomme &ts) const
{ return valeur() <= ts.valeur(); }
bool operator> (const TiteSomme &ts) const
{ return valeur() > ts.valeur(); }
bool operator>=(const TiteSomme &ts) const
{ return valeur() >= ts.valeur(); }
bool operator==(const TiteSomme &ts) const
{ return valeur() == ts.valeur(); }
bool operator!=(const TiteSomme &ts) const
{ return valeur() != ts.valeur(); }
};
int main() {
Comparable<int> c0; // Ok; int est Comparable ...
Comparable<std::string> c1; // Ok ...
Comparable<Incomparable> s3; // Erreur!!!
Comparable<TiteSomme> s4; // Ok...
}
Le secret de la technique repose sur deux pivots :
On s'en doute, cette technique est d'une utilité restreinte si on la prend seule. Elle devient extrêmement utile dans la conception de bibliothèques ou de classes génériques, par contre, là où la validation de contraintes à la compilation devient un critère d'efficacité.