À propos des qualifications static et extern en C++

Ce qui suit est une adaptation d'une réponse à une question du chic Éric Gagnon, inscrit au cours IFT729 à l'Université de Sherbrooke à l'hiver 2012. Sa question portait sur le rôle du mot clé extern, mais pour expliquer ce dernier il faut aussi expliquer le mot clé static, du moins à mon humble avis.

Le mot extern, comme le mot static, existe d'abord et avant tout pour contrôler la visibilité d'un nom lors de l'édition des liens :

Pour comprendre le rôle de ces deux qualifications, imaginez ceci :

a.h a.cpp principal.cpp
#ifndef A_H
#define A_H
int x = 3; // très mauvaise idée
#endif
#include "a.h"
#include "a.h"
int main()
{
}

Nous aurions ici une erreur d'édition des liens, car l'inclusion lexicale de a.h dans a.cpp insère le texte de a.h dans a.cpp et y définit le nom global x associé à un int, et l'inclusion de a.h dans principal.cpp fait de même à cet endroit. Conséquence, il y a deux int x globaux au moment de l'édition des liens, une violation directe de la règle ODR (One Definition Rule).

Une « solution » possible serait d'apposer à x la qualification static, en changeant a.h pour que ce fichier devienne :

#ifndef A_H
#define A_H
static int x = 3; // pas fort non plus, sauf exception
#endif

À ce moment, les deux .cpp (a.cpp et principal.cpp) compileraient, mais les variables x vues par l'un et par l'autre seraient des variables distinctes l'une de l'autre (si main() devait incrémenter son x, cela n'aurait pas d'impact sur le x de a.cpp qui est une autre bestiole). Ça peut surprendre, disons.

Une autre « solution » possible serait d'apposer à x la qualification extern. Ceci impliquerait deux changements, soit un à a.h qui deviendrait :

#ifndef A_H
#define A_H
extern int x; // ceci est une déclaration mais n'est pas une définition
#endif

...et un à a.cpp (par exemple) qui deviendrait :

#include "a.h"
int x = 3; // ceci est une définition

Ici, le nom x, associé à un int global, est visible à toutes les unités de traduction (tous les .cpp du projet) mais une seule définition en existe, celle que j'ai placée dans a.cpp pour cet exemple.

Ces définitions sont très informelles...

Une déclaration, c'est un peu comme un prototype de fonction : ça dit ce qu'un nom est mais ça ne l'instancie pas. Une définition, c'est (en gros) un appel de constructeur, si on veut, et il n'en faut qu'une seule par objet (ici, si un autre .cpp définit un int x global, on aura encore une fois une erreur à l'édition des liens).

Contrairement à l'exemple précédent, qui utilisait un int qualifié static, celui-ci (x qualifié extern) partage un seul et même x entre les divers fichiers sources qui en connaissent la déclaration. C'est beaucoup moins utilisé maintenant qu'à une autre époque parce que les variables globales, ça tend à très mal coexister avec les systèmes à plusieurs processeurs et avec le code multiprogrammé. La qualification extern peut être sympathique avec des constantes, par exemple si nous souhaitons exposer un nom dans un .h mais pas la valeur qui lui est associée.

Les fonctions globales qualifiées static sont surtout utiles en C, où on ne peut avoir deux fonctions du même nom, peu importe le nombre de paramètres, leurs types et ainsi de suite. Si quelqu'un a écrit une petite fonction comme :

int f() { return 3; }

dans un fichier .c quelque part, cela entraînera des conflits à l'édition des liens avec toute autre fonction f() définie dans un autre .c du même projet, même s'il n'y a pas de prototype pour cette fonction, du moins avec les compilateurs C que j'ai utilisés (je n'ai pas joué avec C99 ou avec C11). C'est agaçant en l'absence d'espaces nommés ou d'autres techniques pour contraindre la portée des noms.

Pour cette raison, en C, on écrira plutôt ceci si f() est un outil interne à un fichier source, plutôt qu'une fonction à partager avec les autres :

static int f() { return 3; }

Cela règle le problème, en général.

Enfin, avec les classes, il y a des subtilités :

//
// inclusions, using, #define... omis par souci d'économie
//
class X
{
    static int n = 3; // illégal
    static const int N = 3; // semi-légal
    enum { M = 3 }; // légal
    static float f = 3.14159f; // illégal
    static const float F = 3.14159f; // illégal
    static string fct0()
       { return "Yo"; }
    static string fct1()
    {
       static const string TXT = "Ya";
       return TXT;
    }
    void exec()
    {
       static int nappels = 0;
       ++nappels;
    }
};

L'idée générale est qu'un membre (attribut ou méthode) dans une classe (ou un struct) sera un membre d'instance, appartenant en propre à chaque instance de la classe et ayant une existence distincte pour elle. Si on lui accole la qualification static, elle devient un membre de classe, appartenant à la classe en soi plutôt qu'à ses instances. À mon humble avis, ce choix terminologique visait à faire l'économie d'un mot clé, mais je spécule ici.

Dans l'ordre :

Le cas de X::exec() est intéressant, au sens où tel qu'il est écrit, il s'agit sans doute d'une erreur de logique. En effet, techniquement, nappels est une variable globale (tous les X la partagent, même si elle est dans une méthode d'instance). et l'opérateur ++ sur un entier n'est pas atomique. Ainsi, si plusieurs appels à exec() sur diverses instances de X ont lieu concurremment, il est possible que nappels ait par la suite une valeur incorrecte.

Toutefois, dans du code C ou C++ monoprogrammé, on voyait parfois l'idiome suivant :

void f()
{
   static bool first_pass = true;
   if (first_pass)
   {
      // initialisation, à ne faire qu'une seule fois
      first_pass = false;
   }
   // code à faire lors de chaque appel
}

dans les fonctions pour que du code puisse n'être exécuté que lors du tout premier appel.

Si nous souhaitons revenir à la classe X plus haut, et faire en sorte que cette classe compile correctement, nous pouvons écrire ceci :

X.h X.cpp
#ifndef X_H
#define X_H
#include <string>
class X
{
    static int n; // déclaration
    static const int N = 3; // déclaration + « définition »
    enum { M = 3 }; // déclaration + définition
    static float f; // déclaration
    static const float F; // déclaration
    static string fct0()
       { return "Yo"; }
    static string fct1()
    {
       static const string TXT = "Ya";
       return TXT;
    }
    void exec()
    {
       static int nappels = 0;
       ++nappels;
    }
};
#endif
#include "X.h"
int X::n = 3; // définition
float X::f = 3.14159f; // définition
const float X::F = 3.14159f; // définition

...ou encore, si nous souhaitons vraiment une portabilité à toute épreuve, nous pouvons écrire cela :

X.h X.cpp
#ifndef X_H
#define X_H
#include <string>
class X
{
    static int n; // déclaration
    static const int N; // déclaration
    enum { M = 3 }; // déclaration + définition
    static float f; // déclaration
    static const float F; // déclaration
    static string fct0()
       { return "Yo"; }
    static string fct1()
    {
       static const string TXT = "Ya";
       return TXT;
    }
    void exec()
    {
       static int nappels = 0;
       ++nappels;
    }
};
#endif
#include "X.h"
int X::n = 3; // définition
const int X::N = 3; // définition
float X::f = 3.14159f; // définition
const float X::F = 3.14159f; // définition

......mais en pratique, c'est probablement abusif car la plupart des compilateurs tolèrent, à ma connaissance, la définition immédiate des constantes de classe entières.

Les déclarations extern "C" (entre autres)

Il arrive que l'on voit apparaître dans du code C++ des déclarations telles que la suivante :

extern "C"
{
   int f(int);
   float g(int, double);
}

Ici, le mot extern prend un sens légèrement différent : celui d'indiquer au compilateur que les fonctions entre les accolades (f() prenant en paramètre un int et g() prenant en paramètre un int puis un double, dans l'ordre) sont générées selon la convention d'un autre langage que C++ – ici, et presque toujours en pratique, on parle de la convention C, d'où le "C" suivant le mot extern.

Les conventions de nommage exactes varient selon les compilateurs, mais C est le langage de référence pour l'interopérabilité, dû à la simplicité de son modèle.

Notez que C++ permet à plusieurs fonctions d'avoir le même nom mais des signatures différentes, ce qui force le compilateur à générer des noms dits « décorés » dans le code machine, pas des noms « bruts ». Ainsi, une fonction comme int f(int) en C++ pourrait se nommer en pratique ?f@@YAHH@Z (c'est le nom généré par Visual Studio 2010, sérieusement) au niveau machine, pour être distincte d'autres fonctions f() possibles comme f(double), f(void) ou f(string,int&) par exemple. En C, dans le code machine généré lors de la compilation, la fonction f(int) se nommera probablement f ou _f, tout simplement, car elle seule pourra porter ce nom.

On utilisera donc typiquement extern "C" { /*...*/ } pour deux raisons :

En espérant que le tout, sans être exhaustif, vous soit utile.


Valid XHTML 1.0 Transitional

CSS Valide !