De la portabilité beau, bon, pas cher

Imaginons que nous ayons un sous-programme f() (remplacez ce nom générique par un nom significatif!) qui ait des particularités telles qu'il sera différent selon les configurations du programme, typiquement parce qu'il dépend de directives à la précompilation.

Un exemple d'une telle situation serait le cas où le code de f() est placé entre les directives #ifdef DEBUG et #endif. Dans un tel cas, le corps du sous-programme f() ne serait pertinent qu'en période de test, et on souhaiterait que le code disparaisse lorsque la génération du code de production sera réalisée.

#ifndef _TRUCSMUCHES_H
#define _TRUCSMUCHES_H

//...

void f();

//...

#endif
#include "TrucsMuches.h"
//...
void f ()
{
#ifdef DEBUG
   // le code à exécuter
#endif
}

On ne veut évidemment pas que le code client doive se préoccuper d'appeler une version de test ou une version de production du sous-programme f(). Il faut que le code client soit stable et que seule la présence ou l'absence de la directive de précompilation fasse en sorte que la bonne version du code soit générée.

La manière simple d'arriver à cette fin est de déclarer le prototype de la fonction dans un fichier d'en-tête et de placer la définition de la fonction dans un fichier source. Cette stratégie isole le code client du code serveur du fait que le client appelle toujours f() et que f() est tout simplement vide s'il n'est pas pertinent pour lui de réaliser une tâche.

En retour, cette stratégie pose problème : C++ procède par compilation séparée, donc le compilateur ne voit pas à la génération du code client de f() si f() contient ou non des opérations pertinentes. Si le compilateur voyait que f() est vide, l'appel à f() pourrait être éliminé complètement, un avantage net pour du code de production qui n'a, par définition, pas de temps à perdre.

Notez que l'optimisation peut être faite par certains compilateurs et dans certaines circonstances; il est simplement impossible de la garantir.

En situation de compilation séparée, le compilateur doit s'en remettre à l'éditeur de liens pour la résolution de certains appels de sous-programmes, ce qui fait qu'il n'est pas en mesure de pouvoir garantir en tout temps l'optimisation d'appels potentiellement inutiles.

On voudrait que le code à optimiser (du moins potentiellement) soit visible au compilateur. Cependant, en partie à cause de l'héritage C de C++, pour lequel les directives d'inclusion de fichiers (les #include) ne font qu'une inclusion lexicale d'un fichier, définir un sous-programme dans un fichier d'en-tête qui ne soit ni un template, ni une méthode provoque un risque de définition multiple de ce sous-programme (une définition par fichier source incluant ce fichier d'en-tête), donc d'erreurs lors de l'édition des liens.

Comment faire pour que cette stratégie soit possible ne coûte rien? Simple: avoir recours à un mécanisme qui rende le code à optimiser visible pour le compilateur. En C++, il nous faudra jouer un tour au compilateur et encapsuler l'invocation du sous-programme visé dans une construction plus moderne, par exemple un struct ou une class (par contre, un namespace ne suffira pas).

#ifndef _CONFIG_H
#define _CONFIG_H

struct Config
{
   static void f ( /*params*/ )
   {
#ifdef DEBUG
      f_impl ( /*params*/ );
#endif
   }
private:
   static void f_impl ( /*params*/ );
};
#endif
//
// Config.cpp
//
#include "Config.h"
void Config::f_impl ( /* params */ )
{
   // ... code ...
}
//
// Programme de test
// 
#include <iostream>
#include "Config.h"
int main()
{
   //...
   Config::f ( /* ... */ );
   //...
}

Présumant que nous ayons placé f() dans une struct nommée Config (le nom n'a pas d'importance dans la mesure où il est significatif dans le programme). Puisque f() était à l'origine une fonction globale, nous en ferons ici une méthode de classe (static), évitant ainsi au code client la tâche d'instancier inutilement Config.

Nous ferons de f(), nom de la méthode qui sera invoquée, un simple enrobage autour d'une autre fonction, nommée ici f_impl(), qui fera le véritable travail. L'invocation de f_impl() par f() sera visible au compilateur dans tous les cas puisqu'elle apparaîtra dans le fichier d'en-tête où f() est à la fois déclarée et définie.

Le compilateur sera donc à même de constater que f() est en fait vide lorsque la directive de précompilation DEBUG sera définie. Ce faisant, il éliminera sans peine (et sans dépendre d'un tiers incertain) l'invocation de f() dans tous les cas où cela s'avère pertinent.

Le nom Config::f_impl() est visible aux programmeuses et aux programmeurs mais pas au code client de Config puisqu'il s'agit d'un nom privé. Ce faisant, cette stratégie ne génère pas de dépendances ou de noms supplémentaires dans l'espace accessible au code client.

Résultat: le texte du code client est stable dans sa version de test comme dans sa version de production; seuls les sous-programmes pertinents sont invoqués; et le compilateur peut sans peine offrir les garanties d'optimisation auxquelles nous serions en droit de nous attendre.


Valid XHTML 1.0 Transitional

CSS Valide !