05 oct. 2006

L'hérésie des variables globales

C'est l'un des points les plus souvent cités par les professeurs qui enseignent la programmation, à tel point qu'il en devient un mantra, un dogme à réciter le plus ouvent possible afin de se convraincre de sa véracité :

De variables globales tu n'useras point

La raison en est simple : les variables globales sont extrêmement difficiles à gérer dans le contexte d'un programme complexe. Intrinsèquement, leur utilisation n'est pas thread-safe puisqu'elle sont partagées par l'ensemble des modules composants le programme ; cette caractéristique signifie aussi que les changements de valeur d'une variable globale sont difficile à suivre dans le cadre d'un programme multi-thread ou même, simplement, dans le cadre d'un programme réagissant aux évenements utilisateurs (c'est à dire dont l'éxécution n'est pas linéaire comme c'est le cas pour un programme en mode console). Pire, selon le langages utilisés, l'ordre de construction des variables globales peut ne pas être garanti, ce qui suppose qu'une variable globale ne peut pas forcément faire référence à une autre.

Bref : la variable globale, c'est une sorte de Grand Satan, un antechrist dont la seule vision peut provoquer des cauchemars à l'apprenti ingénieur - et certainement un arrêt cardiaque à son professeur. D'où le mantra, repris en force dans tous les livres et autres documents de programmation sortis à ce jour dans le monde civilisé : variables globales pas bien, jamais, sinon aie aie aie. Les librairies bien construites n'ont pas de variables globales - elles sont une hérésie.

Il y a des variables globales dans la librarie standard du C++.

Je vous laisse le temps d'analyser cette phrase. Oui, il y a des variables globales dans la librairie standard du C++. std::cout, std::cin, std:cerr, std::clog - ces quatres objets sont des instances globales d'une classe de flux[1]. Pour autant, personne ne crie au scandale, personne n'hurle son mécontentemment, et les professeurs sus-cités sont fiers de présenter les fonctionalités de gestion de flux du C++ en utilisant ces objets - en oubliant bien souvent de mettre en relation le dogme et ces variables.

Tout a commencé lorsqu'il est devenu certain qu'une utilisation abusive des variables globales était nuisible au projet, non seulement en terme de maintenance mais aussi en terme de gestion de la concurence et de réentrance. De cette constatation, une religion est née pour affirmer que les variables globales sont des enfants du diable. Cette religion, comme toutes les religions, a oublié les prémices de sa fondation et se contente maintenant de mettre toutes les variables globales dans le même sac, sans distinction aucune. Dans un tel contexte, on peut légitimement se demander pourquoi les concepteurs de la librairie standard C++ - une librairie qui, malgré ses défauts connus, n'en reste pas moins l'une des mieux conçue - ont décidé de créer des instances globales de quelques classes. La réponse est simple : toutes les variables globales ne sont pas mauvaises, et l'utilisation d'un certain nombre de règles peuvent permettre de séparer le bon grain de l'ivraie[2].

Premièrement, si la durée de vie de la variable est inférieure à la durée d'éxécution du programme (autrement dit, si il y a un moment dans le programme où cette variable est invalide, soit parce qu'elle n'a pas encore été initialisée, soit parce qu'elle a déja été détruite) alors cette variable ne doit pas être globale non plus. La raison invoquée ici est simple : quel intérêt représente une variable globale si elle n'est pas complètement globale ? Le danger est grand de s'en servir alors qu'elle n'est plus valide, ce qui provoquera à coup sur un comportement erroné de l'application. Du fait de son existence, une telle variable est un problème qui ne demande qu'à se produire - autant donc l'éliminer dès le début, et ne pas la créer.

Deuxièmement, une variable globale dont la valeur change est un très mauvais candidat. L'assignation d'une nouvelle valeur impacte l'ensemble du programme, de sorte qu'il n'est pas possible pour une fonction particulière d'être sûre de la validité de cette valeur. De plus, dans un programme multithread, l'état de cette variable ne peut pas être garanti du tout - une fonction peut commencer à utiliser la variable et voire sa valeur changer en cours d'éxécution. Ce type de variable est donc à éviter, voire à proscrire complètement. De fait, toutes les variables qui n'ont pour but que de stocker un état particulier (par opposition à un ensemble d'états) entrent dans cette catégorie, et sont donc à éviter.

Troisièmement, une variable globale dont l'état interne peut être modifié partiellement et de manière indépendante pendant le fonctionement du programme est une variable dangereuse. Que cette modification soit protégée ou non dans le cas d'un programme multithread, il n'en reste pas moins qu'une thread peut modifier l'état de l'objet, qui sera alors utilisé par une autre thread sans savoir que cet état a été modifié. Le danger d'un tel problème est évident : à un instant donné, l'état de l'objet est quantique - comme le chat de Shroedinger, il n'a un état connu que si on le soumet à une inspection, et seulement à ce moment. Tout le reste du temps, son état est inconnu, et il est aisé de comprendre que baser un programme sur une variable dont l'état est inconnu est dangereux.

Bien que crucial, ce point perd une partie de son sens lorsqu'il s'agit de représenter une ressource globale, telle que l'accès à un gestionnaire de périphérique. En effet, la plupart des ressources globales du système sont soumises à ces mêmes restrictions, si bien qu'il est nécessaire de protéger leur accès. Le fait de passer ou non par une variable globale pour les représenter n'a que peu d'incidences sur leur fonctionnement.

On peut confronter les variables globales de la librairie standard à ces quelques règles. La première est satisfaite : les variables sont créées très tôt, et sont détruite très tard. On doit toutefois noter qu'avant d'entrer dans la fonction main(), le programme ne peut être sûr de l'existence des flux - de même qu'il ne peut pas en être sûr après être sorti de main(). Toutefois, la durée de vie de ces variables est suffisament longue pour qu'on puisse les considérer comme véritablement globales[3].

Le second point est plus délicat, car rien n'empêche le programmeur d'assigner une valeur à std::cout ou std::cin. Effectuer une telle opération est toutefois un nonsense, et bien que non formellement interdite par le standard C++, il est peu probable qu'un programmeur en arrive à cette extrémité. Cette condition n'est pas satisfaite dans l'absolu, mais reste vraie dans l'immense majorité des programmes C++[4]. A noter qu'une telle opération a des chances de rendre le programme non compatible avec le standard C++, std::cin.tie() devant retourner la valeur &std::cout - ce qui risque de ne plus être le cas si on affecte à std::cin un autre valeur.

La troisième condition n'est pas satisfaite non plus, mais d'un autre coté les flux représentent une resource système globale - les entrées/sorties standard (stdin, stdout, stderr).

Dans l'ensemble, les variables globales définies par le standard C++ satsifont à ces trois conditions, que ce soit de par leur construction (condition 1), leur utilisation (condition 2) ou leur raison d'être (condition 3). Rien ne nous empêche donc d'en faire des variables globales, à moins qu'une meilleure alternative existe. Toutefois, quelles sont les alternatives aux variables globales ?

  • les singletons. Cette alternative n'est valable que dans le cas où l'on souhaite ne définir qu'une seule instance d'une classe donnée. Ce n'est pas le cas ici puisque std::cout, std::cerr et std::clog sont des instances du même type. De plus, un singleton sera soumis aux même restrictions qu'une variable globale dans note cas.
  • utliser la valeur de retour d'une fonction. A cause de la nature des flux d'E/S (qui utilisent une mémoire tampon), cette fonction devrait retourner un objet dont l'état est dépendant des opérations précédement effectués - la seule possibilité pour l'implémenter est donc de recourir à au moins une variable qui serait soit statique, soit globale, ce qui revient au même. Pour éliminer ce problème, on pourrait bien sur décider que les E/S standard n'ont pas à utiliser une mémoire tampon. Ceci-dit, elles le sont pour des raisons d'efficacité. Il est préférable de faire un seul accès périphérique qui écrira ou lira N octets plutôt que d'en faire N qui porteront chacun sur un octet unique.
  • créer des objets temporaires d'une classe spécifique. Encore une fois, il nous faut pourvoir connaitre l'état précédent du flux avant de pouvoir l'ulisiser. De plus, cet état doit être mis à jour dans toutes les autres instances de la classe lorsque l'objet est détruit - j'ai peur que la solution à mettre en place pour une telle gestion soit encore pire que la solution actuelle.
  • en revenir au C, et utiliser les fonctions d'E/S formatées. Nous quittons là le domaine de la prudence, puisque les fonctions d'E/S formatées du C sont variadiques et font totalement abstraction du type des arguments. Cepandant, il est possible d'utiliser un mécanisme proche de celui utilisé dans la librairie boost::format pour éliminer ce problème. En fait, les appels de fonction peuvent très bien utiliser un système proche du système actuel: un objet stream est passé en paramètre de la fonction, et c'est le contenu de cet objet qui est soit écrit sur le périphérique, soit lu du périphérique puis inséré dans l'objet stream. On notera toutefois que ces fonctions ne résole qu'une partie du problème, puisque la nécessité d'utiliser des E/S à mémoire tampon implique de garder des variables statiques ou globales qui certes ne sont plus accessibles à l'utilisateur mais qui sont encore présentes. Au final, le gain est négligeable par rapport à la sitation actuelle, et l'écriture du programme est légèrement complexifiée (puisqu'il faut appeler la fonction et créer un objet stream avant de l'utiliser).

En fait, il n'existe pas d'alternatives possibles à notre problème qui soient aussi efficaces et faciles d'utilisation que la solution courante. C'est peut être la raison la plus importante qui a poussé à faire de ces 4 variables des variables globales.

Bien entendu, la spécificité des variables globales définies par le standard C++ est à l'origine même de leur implémentation en tant que variable globale. Cette implémentation, bien que sub-optimale par rapport aux canons de la programmation objet, n'en reste pas moins valide et pertinente dans ce cas. Mais ces quelques remarques ne s'étendent pas à l'ensemble des variables globales, car peu d'entre elles peuvent se targuer de passer le test des trois points ci-dessus.

Bien évidemment, ces conclusions sont personnelles, et je n'oserais jamais prétendre détenir la vérité sur la façon dont les variables globales peuvent ou doivent être utilisées. Disons que je me borne à essayer de démontrer que toutes ne sont pas mauvaises, à partir du moment où l'on défini un certain nombre de règles qui écartent les définitions trop dangereuses. En tout état de cause, une utilisation intelligente des variables globales n'est pas un crime - le crime étant perpetré dès lors que cette utilisation devient abusive.

PS: je me réserve le droit d'étendre ce billet, pour la simple raison qu'il soulève encore en moi un certain nombre de questions, et que je doute sincèrement d'avoir fait le tour du problème. Ceci dit, les updates mineures prendront la forme de commentaires.

Notes

[1] on notera que des instances similaires sont utilisées pour gérer les E/S en mode UNICODE: std::wcout, std::wcin, std::wcerr et std::wclog

[2] à noter que les constantes globales satisfont l'ensemble de ces conditions. Mais leur noms implique que ce ne sont pas des variables.

[3] la note 265 du standard C++ (ISO/IEC 14882:1998) précise tout de même: les constructeurs et les destructeurs des objets statiques peuvent accéder à ces objets pour lire depuis l'entrée stdin ou pour écrire sur les sorties stdout et stderr.

[4] et dans le cas elle n'est pas satisfaite dans un programme particulier, la solution la plus simple reste de mettre le programmeur responsable d'une telle bourde au pilori

Commentaires

1. Le vendredi, octobre 6 2006, 17:01 par Stéphane MORAT

Il faut aussi éviter au maximum les données partagées (membre static de classe). Elles posent les mêmes problèmes que ceux décrits dans ton article.

On peut détailler les difficultés d'écrire des tests unitaires avec des variables globales ou partagées :

Voici un exemple :

atester.h

class ATester
{

public:
ATester():

int fct1(int param_1);
int fct2(int param_2) const;

private:
int val1;
static int m_partagee;
};

atester.cpp

int ATester::m_partagee =0;

ATester::ATester()
:val1(0)
{}

int ATester::fct1(int param 1)
{
m_partagee ++;
val1++;
return(m_paratagee+val1+param_1)
}

int ATester::fct2(int param_2) const
{
return(m_partagee+val1+param_2)
}


Pour nos deux fonctions de tests :

//Tests unitaires

void test_fct1()
{
ATester MonObjetaTester;
Assert(MonObjetaTester.fct1(10),12); //OK m_paratagee=1 et val1=1
}

void test_fct2()
{
ATester MonObjetaTester;
Assert(MonObjetaTester.fct2(10),10); //NOK m_paratagee=1 et val1=0
}

Cet exemple simpliste montre que l'analyse à effectuer pour écrire un test unitaire correct est beaucoup plus importante avec une variable partagée.
De plus, en fonction de l’ordre des tests effectués, le résultat n’est pas le même.

Il est toujours possible de considérer les variables globales ou partagées commes des entrées supplémentaires des tests mais ces tests auront une application limitée puisqu'ils masqueront le comportement de ces variables(leur raison d'être) au cours du déroulement du programme.

2. Le mardi, octobre 10 2006, 11:03 par Emmanuel Deloget

Il est vrai que test unitaire + variable globale (je n'ai pas traité le cas des variables de classe statiques, mais il s'agit ni plus ni moins que de variables globales déguisées, avec des restrictions supplémentaires sur l'ordre de construction et de destruction) ne font pas bon ménage...

En fait, je crois que la conclusion de mon billet est : "les variables globales peuvent être utilisées, à condition qu'il n'y ait pas de meilleure alternative". Ou quelque chose comme ça :)

3. Le samedi, août 4 2007, 11:34 par Jalel

Bonjour,

J'ai une forte nécessité d'utiliser une variable globale entre les trois premiers couches du modèle OSI (physique, MAC et réseau). Malheureusement cette variable change dynamiquement à chaque instant.

SVP est ce que vous pouvez me renseigner comment je peux implémenter cette notion de variable globale entre des classes complètement indépendantes et comment se fait l'accès à cette variable.

Merci Bien.
Jalel.

4. Le dimanche, août 5 2007, 22:57 par Christophe Moustier

j'aurais envie de t'aiguiller sur cet article :
blog.emmanueldeloget.com/...

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet