Ce texte est directement inspiré d'une technique proposée par Michael Tedder dans un texte de 2011 : http://altdevblogaday.com/2011/06/27/platform-abstraction-with-cpp-templates/
Lorsque nous souhaitons écrire du code multiplateforme en C ou en C++, deux grandes familles de solutions sont traditionnellement appliquées. L'une dépend de macros, l'autre dépend de compilation séparée et de la séparation de l'interface et de l'implémentation.
Pour illustrer notre propos, nous utiliserons un exemple simplifié cherchant à invoquer un service de suspension volontaire de l'exécution d'un programme. Ce type de service porte un nom différent d'une plateforme à l'autre (Sleep(), sleep(), usleep(), sginap(), System::Threading::Thread::Sleep(), etc.) et offre une sémantique tout aussi variable (on passe à ces fonctions un nombre de secondes, de millisecondes, de microsecondes, de tics, ... Certains sont des sommeils interruptibles, d'autres non, etc.).
La portabilité par des macros a recours à une inclusion conditionnelle en fonction des plateformes. Le code à droite en offre un exemple relativement conforme aux usages (il se peut que je n'aie pas choisi les meilleures fonctions de suspension volontaire dans chaque cas, mais bon, c'est un exemple sans prétention) :
|
|
Notez que les directives de plateformes ne sont pas toujours mutuellement exclusives (p. ex. : si vous compilez pour Android, la macro __ANDROID__ sera définie mais la macro __linux__ le sera aussi, alors mieux vaut organiser les tests en conséquence). Évidemment, on suppose les en-têtes appropriés inclus chaque fois (sinon, au pire, vous pouvez les ajouter, par exemple <windows.h> pour Sleep()). |
De tels échafaudages sont très fréquemment rencontrés en pratique (c'est idiomatique du code C portable), mais sont complexes à maintenir à moyen terme et reposent sur la discipline des programmeurs (il faut s'assurer de bien couvrir toutes les plateformes visées, de tenir à jour les symboles de chaque plateforme, de ne pas faire la moindre erreur de conversion, etc.).
En retour, le code client qui repose sur l'abstraction portable (la « fonction » dodo(ms) dans ce cas-ci) sera tel que ses sources seront portables sur les diverses plateformes supportées.
Une autre pratique répandue est de découpler l'interface de l'implémentation, tout simplement. L'interface est portable, et l'implémentation sollicite des services propres à la plateforme. Dans un tel cas, il est possible d'avoir un fichier source par plateforme, tout comme il est possible d'implémenter le tout en appliquant, « sous la couverture », l'approche par inclusions conditionnelles décrite un peu plus haut. Il n'est d'ailleurs pas rare que les diverses implémentations non-portables soient groupées dans une bibliothèque et liées en bloc à l'exécutable final. Notez au passage que le recours au type int ici pour dénoter un délai en millisecondes est discutable, car peu explicite; un type représentant l'unité de mesure serait nettement préférable. |
|
|
Les macros ont le gros défaut d'échapper au compilateur. Le préprocesseur les traite avant que la compilation n'ait lieu, ce qui complique le développement. Une alternative intéressante est de procéder de manière générique.
Pour être efficaces, supposons que votre entreprise utilise des symboles en fonction des plateformes pour lesquelles vous compilez. Ici, j'utiliserai des types plutôt que des constantes. Vous auriez aussi pu utiliser des constantes statiques entières, au choix. Il est possible que j'aie mal ciblé mes plateformes (par exemple, peut-être aurait-il été préférable de remplacer PlateformeWindows par un template tel que PlateformWindows<int N> où N pourrait être 32 ou 64, pour mieux profiter de certains services). Nous terminons le tout par une sélection du type (ou de la constante) approprié(e) pour la plateforme de destination. C'est ce nom, Plateforme, que nous utiliserons par la suite. |
|
Par la suite, nous pouvons spécialiser les services à offrir en fonction des plateformes, sans avoir recours à des macros. Vous trouverez à droite un synopsis de la technique. L'exemple à droite suppose ici encore que les inclusions propres à la plateforme utilisée ont été faites. Vous remarquerez ici que nous obtiendrons le même degré très direct d'accès aux services (inlining possible, entre autres) qu'avec les macros, mais sans toutefois introduire une dépendance envers un mécanisme externe au compilateur. Le code non-portable des versions inutilisées des divers services ne sera pas généré, selon le principe SFINAE, et ne causera pas d'erreurs à la compilation dans la mesure où il est syntaxiquement correct. Il aurait aussi été possible de séparer l'interface de l'implémentation et de placer le code des fonctions de sommeil dans un fichier source distinct. La définition du type sommeil en fin de parcours est une simplification syntaxiqueé |
|
Un programme de test correct utilisant le service de sommeil propre à la plateforme serait celui à droite. Pas mal, n'est-ce pas? |
|