IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Penser en C++

Volume 1


précédentsommairesuivant

XI. Contrôle du nom

Créer des noms est une activité fondamentale en programmation, et quand un projet devient important, le nombre de noms peut aisément être accablant.

C++ vous permet un grand choix de contrôle sur la création et la visibilité des noms, sur l'endroit où sont stockés ces noms, et la liaison entre ces noms.

Le mot-clé static était surchargé en C avant même que les gens sachent ce que le terme “surcharger (overload)” signifiait, et C++ ajoute encore un autre sens. Le concept fondamental que tout le monde utilise de static semble être “quelque chose qui reste à sa position » (comme l'électricité statique), ce qui signifie une location physique dans la mémoire ou visible dans un fichier.

Dans ce chapitre, vous apprendrez comment mettre en place un contrôle de stockage et de visibilité static, et une façon d'améliorer le contrôle d'accès aux noms via le dispositif C++ namespace. Vous trouverez aussi comment utiliser des fonctions écrites et compilées en C.

XI-A. Eléments statiques issus du C

En C comme en C++ le mot-clé static a deux sens de base, qui malheureusement se recoupent souvent l'un avec l'autre:

  1. Alloué une seule fois à une adresse fixe; c'est-à-dire, l'objet est créé dans une zone de données statiques spéciale plutôt que sur la pile à chaque fois qu'une fonction est appelée. C'est le concept de stockage en mémoire statique.
  2. Local à une unité de compilation particulière (et local à la portée d'une classe en C++, ce que vous verrez plus tard). Ici, static contrôle la visibilité du nom, de manière à ce que ce nom ne puisse pas être vu en dehors de l'unité de compilation ou de la classe. Ceci décrit aussi le concept de liaison, qui détermine quels noms l'éditeur de liens verra.

Cette section va se pencher sur les sens de static présentés ci-dessus tels qu'ils ont hérité du C.

XI-A-1. Variables statiques à l'intérieur des fonctions

Quand vous créez une variable locale à l'intérieur d'une fonction, le compilateur alloue de l'emplacement mémoire pour cette variable à chaque fois que la fonction est appelée en déplaçant le pointeur de pile vers le bas autant qu'il le faut. S'il existe un initialiseur pour cette variable, l'initialisation est effectuée chaque fois que cette séquence est exécutée.

Parfois, cependant, vous voulez conserver une valeur entre les différents appels d'une fonction. Vous pourriez le faire au moyen d'une variable globale, mais alors cette variable ne serait plus sous le contrôle de cette seule fonction. C et C++ vous permettent de créer un objet static à l'intérieur d'une fonction; le stockage de cet objet en mémoire ne s'effectue alors plus sur la pile, mais a lieu dans la zone des données statiques du programme. Cet objet est initialisé seulement une fois, la première fois que la fonction est appelée, et puis il conserve sa valeur entre chaque appel de fonction. Par exemple, la fonction ci-dessous retourne le prochain caractère du tableau à chaque fois que la fonction est appelée :

 
Sélectionnez
//: C10:StaticVariablesInfunctions.cpp
#include "../require.h"
#include <iostream>
using namespace std;
 
char oneChar(const char* charArray = 0) {
  static const char* s;
  if(charArray) {
    s = charArray;
    return *s;
  }
  else
    require(s, "s n'est pas initialise");
  if(*s == '\0')
    return 0;
  return *s++;
}
 
char* a = "abcdefghijklmnopqrstuvwxyz";
 
int main() {
  // oneChar(); // require() echoue
  oneChar(a); // Initialise s a la valeur a
  char c;
  while((c = oneChar()) != 0)
    cout << c << endl;
} ///:~

La variable static char* s conserve sa valeur entre les appels de oneChar( ) parce que son enregistrement en mémoire n'a pas lieu sur la pile de la fonction, mais dans la zone de données statiques du programme. Quand vous appelez oneChar( ) avec un argument char*, s est affecté à cet argument, et le premier caractère du tableau est retourné. Chaque appel suivant de oneChar( )sans argument produit la valeur zéro par défaut pour charArray, ce qui indique à la fonction que vous êtes toujours en train d'extraire des caractères depuis la valeur de s précédemment initialisée. La fonction continuera de fournir des caractères jusqu'à ce qu'elle atteigne le caractère nul de terminaison du tableau. Elle s'arrêtera alors d'incrémenter le pointeur afin de ne pas déborder la fin du tableau.

Mais qu'arrive-t-il si vous appelez oneChar( ) sans aucun argument et sans avoir au préalable initialisé la valeur s? Dans la définition de s, vous pourriez avoir fourni un initialiseur,

 
Sélectionnez
static char* s = 0;

Mais si vous ne fournissez pas d'initialiseur pour une variable statique d'un type intégré, le compilateur garantit que cette variable sera initialisée à la valeur zéro (convertie dans le type qui convient) au démarrage du programme. Donc dans oneChar( ), la première fois que la fonction est appelée, s vaut zéro. Dans ces conditions, le test if(!s) réussira.

L'initialisation ci-dessus pour s est vraiment simple, mais l'initialisation des objets statiques (comme pour tous les autres objets) peut consister en des expressions arbitraires mettant en jeu des constantes ainsi que des fonctions et des variables précédemment déclarées.

Soyez conscients que la fonction ci-dessus est particulièrement vulnérable aux problèmes de multitâche ; à chaque fois que vous concevez des fonctions contenant des variables statiques vous devriez avoir les problématiques de multitâche à l'esprit.

Objets statiques à l'intérieur des fonctions

Les règles sont les mêmes pour un objet statique de type défini par l'utilisateur, y compris le fait que certaines initialisations sont requises pour cet objet. Cependant, l'affectation à zéro n'a un sens que pour les types de base ; les types définis par l'utilisateur doivent être initialisés avec l'appel du constructeur. Ainsi, si vous ne spécifiez pas d'argument au constructeur quand vous définissez un objet statique, la classe doit avoir un constructeur par défaut. Par exemple,

 
Sélectionnez
//: C10:StaticObjectsInFunctions.cpp
#include <iostream>
using namespace std;
 
class X {
  int i;
public:
  X(int ii = 0) : i(ii) {} // Défaut
  ~X() { cout << "X::~X()" << endl; }
};
 
void f() {
  static X x1(47);
  static X x2; // Constructeur par défaut requis
}
 
int main() {
  f();
} ///:~

L'objet statique de type X à l'intérieur de f( ) peut être initialisé soit avec la liste d'arguments du constructeur soit avec le constructeur par défaut. Cette construction se produit au premier passage sur la définition, et seulement la première fois.

Destructeur d'objet statique

Les destructeurs pour les objets statiques (c'est-à-dire, tous les objets avec un stockage statique, pas uniquement les objets locaux statiques comme dans l'exemple précédent) sont appelés quand on sort du main( ) ou quand la fonction de la librairie standard du C exit( ) est explicitement appelée. Dans la plupart des implémentations, main( ) appelle simplement exit( ) quand il se termine. Ce qui signifie que cela peut être dangereux d'appeler exit( ) à l'intérieur d'un destructeur parce que vous pouvez aboutir à une récursivité infinie. Les destructeurs d'objets statiques ne sont pas appelés si vous sortez du programme en utilisant la fonction de la librairie standard du C abort( ).

Vous pouvez spécifier les actions à mettre en place quand vous quittez le main( )(ou quand vous appelez exit( )) en utilisant la fonction de la librairie standard C atexit( ). Dans ce cas, les fonctions enregistrées par atexit( ) peuvent être appelées avant le destructeur de n'importe quel objet, avant de quitter le main( )(ou d'appeler exit( )).

Comme les destructions ordinaires, la destruction des objets statiques se produit dans l'ordre inverse de l'initialisation. Cependant, seuls les objets qui ont été construits sont détruits. Heureusement, les outils de développement gardent la trace de l'ordre d'initialisation et des objets qui ont été construits. Les objets globaux sont toujours construits avant de rentrer dans le main( ) et détruits à la sortie du main( ), mais si une fonction contenant un objet statique local n'est jamais appelée, le constructeur pour cet objet n'est jamais exécuté, donc le destructeur n'est aussi jamais exécuté. Par exemple,

 
Sélectionnez
//: C10:StaticDestructors.cpp
// Destructeurs d'objets statiques
#include <fstream>
using namespace std;
ofstream out("statdest.out"); // fichier de trace
 
class Obj {
  char c; // Identifiant
public:
  Obj(char cc) : c(cc) {
    out << "Obj::Obj() pour " << c << endl;
  }
  ~Obj() {
    out << "Obj::~Obj() pour " << c << endl;
  }
};
 
Obj a('a'); // Global (stockage statique)
// Constructeur & destructeur sont toujours appelés
 
void f() {
  static Obj b('b');
}
 
void g() {
  static Obj c('c');
}
 
int main() {
  out << "début de main()" << endl;
  f(); // Appelle le constructeur statique de b
  // g() n'est pas appelé
  out << "fin de main()" << endl;
} ///:~

Dans Obj, le char c joue le rôle d'un identifiant afin que le constructeur et le destructeur puissent afficher des informations sur les objets avec lesquels ils travaillent. L' Obj a est un objet global, donc le constructeur est toujours appelé pour lui avant l'entrée dans le main( ), mais le constructeur pour le static Obj b dans f( ) et le static Obj c dans g( ) ne sont appelés que si ces fonctions sont appelées.

Pour démontrer quels constructeurs et destructeurs sont appelés, seule f( ) est appelée. La sortie du programme est

 
Sélectionnez
Obj::Obj() for a
début de main()
Obj::Obj() for b
fin de main()
Obj::~Obj() for b
Obj::~Obj() for a

Le constructeur pour a est appelé avant d'entrer dans le main( ), et le constructeur pour b est appelé seulement parce que f( ) est appelé. Quand on sort du main( ), les destructeurs pour les objets qui ont été construits sont appelés dans le sens inverse de l'ordre de construction. Ce qui signifie que si g( )est appelé, l'ordre dans lequel les destructeurs sont appelés pour b et c dépend de qui de f( ) ou g( ) a été appelé en premier.

Notez que l'objet out du fichier de trace ofstream est aussi un objet statique – puisqu'il est défini en dehors de toute fonction, il réside dans la zone de stockage statique. Il est important que sa définition (contrairement à une déclaration extern) apparaisse au début du fichier, avant toute utilisation possible de out. Sinon, vous utiliseriez un objet avant de l'avoir correctement initialisé.

En C++, le constructeur d'un objet statique global est appelé avant d'entrer dans le main( ), ainsi vous avez maintenant une façon simple et portable d'exécuter du code avant d'entrer dans le main( ) et d'exécuter du code avec le destructeur après la sortie du main( ). En C, cela représentait une difficulté qui nécessitait toujours de manipuler le code assembleur de lancement de programmes du compilateur.

XI-A-2. Contrôle de l'édition de liens

Ordinairement, tout nom dont la portée est le fichier(c'est-à-dire qui n'est pas inclus dans une classe ou une fonction) est visible à travers toutes les unités de compilation dans le programme. Ceci est souvent appelé liaison externe parce qu'au moment de la liaison le nom est visible partout pour l'éditeur de liens, extérieur à cette unité de compilation. Les variables globales et les fonctions ordinaires sont à liaison externe.

Parfois, vous aimeriez limiter la visibilité d'un nom. Vous pourriez vouloir donner à une variable la portée fichier pour que toutes les fonctions dans ce fichier puissent l'utiliser, mais vous voudriez que les fonctions en dehors de ce fichier ne puissent pas voir ou accéder à cette variable, ou créer par inadvertance des conflits de noms en dehors du fichier.

Un objet ou un nom de fonction avec la portée fichier qui est explicitement déclaré static est local à son unité de compilation (selon les termes de ce livre, le fichier cpp où la déclaration a été faite). Ce nom a une liaison interne. Ceci signifie que vous pouvez utiliser le même nom dans d'autres unités de compilation sans conflit de noms.

Un avantage de la liaison interne est que le nom peut être placé dans l'entête du fichier sans se soucier qu'il puisse y avoir un conflit au moment de la liaison. Les noms qui sont en général placés dans les fichiers d'en-tête, comme les définitions de const et les fonctions inline, sont par défaut à liaison interne. (Toutefois, le const n'est à liaison interne par défaut qu'en C++ ; en C il est à liaison externe par défaut.) Notez que la liaison se réfère uniquement aux éléments qui ont une adresse au moment de l'édition de lien ou au chargement ; aussi les déclarations de classe et les variables locales n'ont pas de liaison.

Confusion

Voici un exemple de comment les deux sens de static peuvent se télescoper. Tous les objets globaux ont implicitement une classe de stockage statique, donc si vous dites (dans la portée du fichier),

 
Sélectionnez
int a = 0;

alors le stockage de a sera dans la zone des données statiques du programme, et l'initialisation pour a aura lieu une seule fois, avant d'entrer dans le main( ). De plus, la visibilité de a est globale à travers l'unité de compilation. En termes de visibilité, l'opposé de static(visible uniquement dans l'unité de compilation) est extern, ce qui signifie explicitement que le nom est visible dans toutes les unités de compilation. Donc la définition précédente est équivalente à

 
Sélectionnez
extern int a = 0;

Mais si vous dites au lieu de cela,

 
Sélectionnez
static int a = 0;

tout ce que vous avez fait est de changer la visibilité, afin que a ait une liaison interne. La classe de stockage n'est pas affectée – l'objet réside dans la zone des données statiques que la visibilité soit static ou extern.

Une fois que vous entrez dans des variables locales, static n'altère plus la visibilité et au lieu de cela affecte la classe de stockage.

Si vous déclarez extern ce qui apparaît comme une variable locale, cela signifie que le stockage existe ailleurs (donc la variable est de fait globale pour la fonction). Par exemple :

 
Sélectionnez
//: C10:LocalExtern.cpp
//{L} LocalExtern2
#include <iostream>
 
int main() {
  extern int i;
  std::cout << i;
} ///:~
 
//: C10:LocalExtern2.cpp {O}
int i = 5;
///:~

Avec les noms de fonctions (pour les fonctions non membre), static et extern peuvent seulement changer la visibilité, donc si vous dites

 
Sélectionnez
extern void f();

C'est la même chose que la simple déclaration

 
Sélectionnez
void f();

et si vous dites,

 
Sélectionnez
static void f();

Cela signifie que f( ) est visible seulement dans l'unité de compilation – ceci est parfois appelé statique au fichier.

XI-A-3. Autres spécificateurs de classe de stockage

Vous verrez souvent static et extern. Deux autres spécificateurs de classe de stockage apparaissent moins souvent. Le spécificateur auto n'est presque jamais utilisé parce qu'il dit au compilateur que c'est une variable locale. auto est une abréviation de “automatique” et cela fait référence à la façon dont le compilateur alloue automatiquement le stockage pour la variable. Le compilateur peut toujours déterminer ceci en fonction du contexte dans lequel la variable est définie, donc auto est redondant.

Une variable register est une variable locale ( auto), avec une indication au compilateur que cette variable sera fortement utilisée et le compilateur devrait la conserver, si possible, dans un registre. Ainsi, c'est une aide à l'optimisation. Les divers compilateurs répondent différemment à ce conseil ; ils ont la possibilité de l'ignorer. Si vous prenez l'adresse de la variable, le spécificateur register sera presque certainement ignoré. Vous devriez éviter d'utiliser register parce que le compilateur fera en général un meilleur travail d'optimisation que vous.

XI-B. Les namespaces

Bien que les noms puissent être dissimulés dans les classes, les noms des fonctions globales, des variables globales et des classes sont toujours dans espace de nommage global unique. Le mot-clé static vous donne un certain contrôle sur cela en vous permettant de donner aux variables et aux fonctions une convention de liaison interne (c'est-à-dire, de les rendre statique dans la portée du fichier). Mais dans un grand projet, un défaut de contrôle sur l'espace de nommage global peut causer des problèmes. Pour résoudre ces problèmes pour les classes, les vendeurs créent souvent des noms longs et compliqués qui ont peu de chance de causer de problème, mais alors vous n'avez d'autre choix que de taper ces noms. (Un typedef est souvent utilisé pour simplifier.) C'est une solution ni élégante, ni supportée par le langage.

Vous pouvez subdiviser l'espace de nommage global en morceaux plus faciles à gérer en utilisant la fonctionnalité namespace de C++. Le mot-clé namespace, similaire à class, struct, enum, et union, place les noms de ses membres dans un espace distinct. Alors que les autres mot-clés ont des buts supplémentaires, la création d'un nouvel espace de nommage est le seul but de namespace.

XI-B-1. Créer un espace de nommage

La création d'un espace de nommage est notablement similaire à la création d'une classe :

 
Sélectionnez
//: C10:MyLib.cpp
namespace MyLib {
  // Declarations
}
int main() {} ///:~

Ce code produit un nouvel espace de nommage contenant les déclarations incluses. Il y a des différences significatives avec class, struct, union et enum, toutefois :

  • La définition d'un espace de nommage ne peut apparaître qu'à la portée globale, ou imbriquée dans un autre espace de nommage.
  • Il n'est pas nécessaire de placer un point virgule après l'accolade de fermeture de la définition d'un espace de nommage.
  • La définition d'un espace de nommage peut être “continuée” sur plusieurs fichiers d'en-tête en utilisant une syntaxe qui, pour une classe, ressemblerait à une redéfinition :
 
Sélectionnez
//: C10:Header1.h
#ifndef HEADER1_H
#define HEADER1_H
namespace MyLib {
  extern int x;
  void f();
  // ...
}
 
Sélectionnez
#endif // HEADER1_H ///:~
//: C10:Header2.h
#ifndef HEADER2_H
#define HEADER2_H
#include "Header1.h"
// Ajoute plus de noms à MyLib
namespace MyLib { // Ceci N'est PAS une redéfinition!
  extern int y;
  void g();
  // ...
}
 
Sélectionnez
#endif // HEADER2_H ///:~
//: C10:Continuation.cpp
#include "Header2.h"
int main() {} ///:~
  • Il est possible de créer un alias pour le nom d'une espace de nommage, si bien que vous n'avez pas à taper un nom peu maniable créé par un vendeur de bibliothèques de fonctions :
 
Sélectionnez
//: C10:BobsSuperDuperLibrary.cpp
namespace BobsSuperDuperLibrary {
  class Widget { /* ... */ };
  class Poppit { /* ... */ };
  // ...
}
// Trop à taper ! Je vais lui créer un alias :
namespace Bob = BobsSuperDuperLibrary;
int main() {} ///:~
  • Vous ne pouvez pas créer une instance d'espace de nommage comme vous pouvez le faire avec une classe.

Espaces de nommage anonymes

Chaque unité de traduction contient un espace de nommage anonyme auquel vous pouvez faire des ajouts en disant « namespace », sans identifiant :

 
Sélectionnez
//: C10:UnnamedNamespaces.cpp
namespace {
  class Arm  { /* ... */ };
  class Leg  { /* ... */ };
  class Head { /* ... */ };
  class Robot {
    Arm arm[4];
    Leg leg[16];
    Head head[3];
    // ...
  } xanthan;
  int i, j, k;
}
int main() {} ///:~

Les noms dans cet espace sont automatiquement disponibles dans cette unité de traduction sans qualification. Un espace de nommage anonyme unique est garanti pour chaque unité de traduction. Si vous mettez des noms locaux dans un espace de nommage anonyme, vous n'avez pas besoin de leur fournir de convention de liens interne en les rendant static.

C++ rend périmée l'utilisation de statiques de fichier en faveur des espaces de nommage anonymes.

Amis

Vous pouvez injecter une déclaration friend dans un espace de nommage en la déclarant au sein d'une classe de ce namespace :

 
Sélectionnez
//: C10:FriendInjection.cpp
namespace Me {
  class Us {
    //...
    friend void you();
  };
} 
int main() {} ///:~

À présent, la fonction you( ) est un membre de l'espace de nommage Me.

Si vous introduisez un ami dans une classe dans l'espace de nommage global, l'ami est injecté au niveau global.

XI-B-2. Utiliser un espace de nommage

Vous pouvez faire référence à un nom dans un espace de nommage de trois façons différentes : en spécifiant le nom en utilisant l'opérateur de résolution de portée, avec une directive using pour introduire tous les noms dans l'espace de nommage, ou bien avec une déclaration using introduisant les noms un par un.

Résolution de portée

Tout nom dans un espace de nommage peut être explicitement spécifié en utilisant l'opérateur de résolution de portée de la même façon que pour référencer les noms dans une classe :

 
Sélectionnez
//: C10:ScopeResolution.cpp
namespace X {
  class Y {
    static int i;
  public:
    void f();
  };
  class Z;
  void func();
}
int X::Y::i = 9;
 
Sélectionnez
class X::Z {
  int u, v, w;
public:
  Z(int i);
  int g();
};
 
Sélectionnez
X::Z::Z(int i) { u = v = w = i; }
int X::Z::g() { return u = v = w = 0; }
 
Sélectionnez
void X::func() {
  X::Z a(1);
  a.g();
}
int main(){} ///:~

Remarquez que la définition X::Y::i pourrait tout aussi facilement faire référence à une donnée membre d'une classe Y imbriquée dans une classe X au lieu d'un espace de nommage X.

Jusqu'ici, les espaces de nommage ressemblent beaucoup aux classes.

L'instruction using

Comme taper le nom complet d'un identifiant dans un espace de nommage peut rapidement devenir fastidieux, le mot-clé using vous permet d'importer un espace de nommage entier en une seule fois. Quand on l'utilise en conjonction avec le mot-clé namespace, on parle d'une directive using. Grâce à la directive using, les noms semblent appartenir au plus proche espace de nommage englobant, si bien que vous pouvez facilement utiliser les noms non qualifiés. Considérez un espace de nommage simple :

 
Sélectionnez
//: C10:NamespaceInt.h
#ifndef NAMESPACEINT_H
#define NAMESPACEINT_H
namespace Int {
  enum sign { positive, negative };
  class Integer {
    int i;
    sign s;
  public:
    Integer(int ii = 0) 
      : i(ii),
        s(i >= 0 ? positive : negative)
    {}
    sign getSign() const { return s; }
    void setSign(sign sgn) { s = sgn; }
    // ...
  };
} 
#endif // NAMESPACEINT_H ///:~

Une des utilisations de la directive using est de porter tous les noms de Int dans un autre espace de nommage, laissant ces noms imbriqués au sein de l'espace de nommage :

 
Sélectionnez
//: C10:NamespaceMath.h
#ifndef NAMESPACEMATH_H
#define NAMESPACEMATH_H
#include "NamespaceInt.h"
namespace Math {
  using namespace Int;
  Integer a, b;
  Integer divide(Integer, Integer);
  // ...
} 
#endif // NAMESPACEMATH_H ///:~

Vous pouvez également déclarer tous les noms situés dans Int dans une fonction, mais laisser ces noms imbriqués dans la fonction :

 
Sélectionnez
//: C10:Arithmetic.cpp
#include "NamespaceInt.h"
void arithmetic() {
  using namespace Int;
  Integer x;
  x.setSign(positive);
}
int main(){} ///:~

Sans l'instruction using, tous les noms dans l'espace de nommage auraient besoin d'être complètement qualifiés.

Un des aspects de la directive using peut sembler un peu contre-intuitif au premier abord. La visibilité des noms introduits en utilisant une directive using est la portée dans laquelle la directive se trouve. Mais vous pouvez redéfinir ces noms par la directive using comme s'ils avaient été déclarés globalement à cette portée !

 
Sélectionnez
//: C10:NamespaceOverriding1.cpp
#include "NamespaceMath.h"
int main() {
  using namespace Math;
  Integer a; // Hides Math::a;
  a.setSign(negative);
  // A présent, la résolution de portée est nécessaire
  // pour sélectionner Math::a :
  Math::a.setSign(positive);
} ///:~

Supposez que vous avez un deuxième espace de nommage qui contient certains des noms contenus dans namespace Math:

 
Sélectionnez
//: C10:NamespaceOverriding2.h
#ifndef NAMESPACEOVERRIDING2_H
#define NAMESPACEOVERRIDING2_H
#include "NamespaceInt.h"
namespace Calculation {
  using namespace Int;
  Integer divide(Integer, Integer);
  // ...
} 
#endif // NAMESPACEOVERRIDING2_H ///:~

Comme cet espace de nommage est également introduit à l'aide d'une directive using, il y a un risque de collision. Toutefois, l'ambiguïté apparaît au niveau où le nom est utilisé, pas à celui de la directive using:

 
Sélectionnez
//: C10:OverridingAmbiguity.cpp
#include "NamespaceMath.h"
#include "NamespaceOverriding2.h"
void s() {
  using namespace Math;
  using namespace Calculation;
  // Tout se passe bien jusqu'à :
  //! divide(1, 2); // Ambiguïté
}
int main() {} ///:~

Ainsi, il est possible d'écrire des directives using pour introduire des espaces de nommage avec des conflits de noms sans jamais produire d'ambiguïté.

La déclaration using

Vous pouvez injecter les noms un à la fois dans la portée courante en utilisant une déclaration using. Contrairement à la directive using, qui traitent les noms comme s'ils étaient déclarés globalement à la portée, une déclaration using est une déclaration dans la portée courante. Ceci signifie qu'elle peut être prioritaire sur les noms d'une directive using:

 
Sélectionnez
//: C10:UsingDeclaration.h
#ifndef USINGDECLARATION_H
#define USINGDECLARATION_H
namespace U {
  inline void f() {}
  inline void g() {}
}
namespace V {
  inline void f() {}
  inline void g() {}
} 
#endif // USINGDECLARATION_H ///:~
 
Sélectionnez
//: C10:UsingDeclaration1.cpp
#include "UsingDeclaration.h"
void h() {
  using namespace U; // Directive using 
  using V::f; // Déclaration using 
  f(); // Calls V::f();
  U::f(); // Doit être complètement qualifié pour l'appel
}
int main() {} ///:~

La déclaration using donne uniquement le nom complètement spécifié de l'identifiant, mais pas d'information de type. Ainsi, si l'espace de nommage contient un ensemble de fonctions surchargées avec le même nom, la déclaration using déclare toutes les fonctions surchargées du même ensemble.

Vous pouvez placer une déclaration using partout où une déclaration normale peut se trouver. Une déclaration using fonctionne comme une déclaration normale à tous points de vue, sauf un : comme vous ne donnez pas de liste d'arguments, il est possible à une déclaration using de causer la surcharge d'une fonction avec les mêmes types d'arguments (ce qui est interdit dans le cas de la surcharge normale). Toutefois, cette ambiguïté ne se manifeste pas au point de la déclaration, mais plutôt au point d'utilisation.

Une déclaration using peut aussi apparaître dans un espace de nommage, et a le même effet que partout ailleurs - ce nom est déclaré dans l'espace de nommage :

 
Sélectionnez
//: C10:UsingDeclaration2.cpp
#include "UsingDeclaration.h"
namespace Q {
  using U::f;
  using V::g;
  // ...
}
void m() {
  using namespace Q;
  f(); // Calls U::f();
  g(); // Calls V::g();
}
int main() {} ///:~

Une déclaration using est un alias, et vous permet de déclarer la même fonction dans des espaces de nommage différents. Si vous vous retrouvez à redéclarer la même fonction en importants différents espaces de nommage, cela ne pose pas de problème - il n'y aura pas d'ambiguïté ni de duplication.

XI-B-3. L'utilisation des espaces de nommage

Certaines des règles ci-dessus peuvent paraître un peu intimidantes à première vue, surtout si vous avez l'impression que vous les utiliserez tout le temps. En général, toutefois, vous pouvez vous en tirer avec une utilisation très simple des espaces de nommage tant que vous comprenez comment ils fonctionnent. Le point clef à se rappeler est que quand vous introduisez une directive using globale (via un « using namespace” hors de toute portée) vous avez ouvert l'espace de nommage pour ce fichier. C'est généralement commode pour un fichier d'implémentation (un fichier « cpp”) parce que la directive using n'a d'effet que jusqu'à la fin de la compilation de ce fichier. C'est-à-dire qu'il n'affecte aucun autre fichier, si bien que vous pouvez ajuster le contrôle de l'espace de nommage un fichier d'implémentation après l'autre. Par exemple, si vous découvrez une collision de noms à cause d'un trop grand nombre de directives using dans un fichier d'implémentation donné, il est facile de le modifier de façon à ce qu'il utilise la qualification explicite ou des déclarations using pour éliminer cette collision, sans modifier d'autres fichiers d'implémentation.

Les fichiers d'en-tête sont autre chose. Vous ne voudrez virtuellement jamais introduire une directive using globale dans un fichier d'en-tête, car cela signifierait que n'importe quel autre fichier qui inclut votre fichier d'en-tête aurait également le même espace de nommage ouvert (et les fichiers d'en-tête peuvent inclure d'autres fichiers d'en-tête).

Dans les fichiers d'en-tête vous devriez donc utiliser soit la qualification explicite soit des directives using incluses dans une portée et des déclarations using. C'est la technique que vous trouverez dans ce livre et en la suivant vous ne « polluerez » pas l'espace de nommage global et éviterez de vous retrouver de retour dans le monde préespace de nommage de C++.

XI-C. Membres statiques en C++

Il arrive que vous ayiez besoin qu'un seul emplacement de stockage soit utilisé par tous les objets d'une classe. En C, vous utiliseriez une variable globale, mais ce n'est pas très sûr. Les données globales peuvent être modifiées par tout le monde, et leurs noms peuvent entrer en conflit avec d'autres dans un grand projet. L'idéal serait de pouvoir stocker les données comme si elles étaient globales, mais dissimulées dans une classe et clairement associées à cette classe.

Ceci peut se faire grâce aux données membres static au sein d'une classe. Il y a un seul emplacement de stockage pour une donnée membre static, indépendamment du nombre d'objets que vous créez de cette classe. Tous les objets partagent le même espace de stockage pour cette donnée membre static, ce qui en fait un moyen de « communication » entre eux. Mais la donnée static appartient à la classe ; la portée de son nom se réalise au sein de la classe et peut être public, private, ou protected.

XI-C-1. Définir le stockage pour les données membres statiques

Comme une donnée static a un seul espace de stockage indépendamment du nombre d'objets créés, cet espace doit être défini dans un endroit unique. Le compilateur n'allouera pas d'espace pour vous. L'éditeur de lien rapportera une erreur si une donnée membre static est déclarée, mais pas définie.

La définition doit avoir lieu en dehors de la classe (l'inlining n'est pas autorisé), et une seule définition est autorisée. Ainsi, il est courant de la mettre dans le fichier d'implémentation de la classe. La syntaxe pose parfois problème pour certains, mais elle est en fait assez logique. Par exemple, si vous créez une donnée membre static dans une classe comme ceci :

 
Sélectionnez
class A {
  static int i;
public:
  //...
};

Alors, vous devez définir le stockage pour cette donnée membre static dans le fichier de définition comme ceci :

 
Sélectionnez
int A::i = 1;

Si vous définissiez une variable globale ordinaire, vous écririez :

 
Sélectionnez
int i = 1;

mais ici, l'opérateur de résolution de portée et le nom de la classe sont utilisés pour spécifier A::i.

Certaines personnes ont des difficultés avec l'idée que A::i est private, et pourtant voici quelque chose qui semble le manipuler librement au grand jour. Est-ce que cela ne brise pas le mécanisme de protection ? C'est une pratique complètement sûre, pour deux raisons. Premièrement, le seul endroit où cette initialisation soit légale est dans la définition. De fait, si la donnée static était un objet avec un constructeur, vous appelleriez le constructeur au lieu d'utiliser l'opérateur =. Deuxièmement, une fois que la définition a eu lieu, l'utilisateur finale ne peut pas faire une deuxième définition - l'éditeur de liens renverrait une erreur. Et le créateur de la classe est forcé de créer la définition ou l'édition de liens du code ne se fera pas pendant le test. Ceci garantit que la définition n'a lieu qu'une fois et qu'elle est effectuée par le créateur de la classe.

L'expression d'initialisation complète pour un membre statique est dans la portée de la classe. Par exemple,

 
Sélectionnez
//: C10:Statinit.cpp
// Portée d'un initialiseur statique
#include <iostream>
using namespace std;
 
int x = 100;
 
class WithStatic {
  static int x;
  static int y;
public:
  void print() const {
    cout << "WithStatic::x = " << x << endl;
    cout << "WithStatic::y = " << y << endl;
  }
};
 
int WithStatic::x = 1;
int WithStatic::y = x + 1;
// WithStatic::x NOT ::x
 
int main() {
  WithStatic ws;
  ws.print();
} ///:~

Ici, le qualificatif WithStatic:: étend la portée de WithStatic à la définition entière.

Initialisation de tableaux statiques

Le chapitre 8 introduit les variables static const qui vous permettent de définir une valeur constante dans le corps d'une classe. Il est possible également de créer des tableaux d'objets static, qu'ils soient const ou non. La syntaxe est raisonnablement cohérente :

 
Sélectionnez
//: C10:StaticArray.cpp
// Initialiser les tableaux statiques dans les classes
class Values {
  // Les static const sont intialisés sur place:
  static const int scSize = 100;
  static const long scLong = 100;
  // Le comptage automatique fonctionne avec les tableaux statiques.
  // Les tableaux, non intégraux et statiques non-const
  // doivent être intialisés extérieurement :
  static const int scInts[];
  static const long scLongs[];
  static const float scTable[];
  static const char scLetters[];
  static int size;
  static const float scFloat;
  static float table[];
  static char letters[];
};
 
int Values::size = 100;
const float Values::scFloat = 1.1;
 
const int Values::scInts[] = {
  99, 47, 33, 11, 7
};
 
const long Values::scLongs[] = {
  99, 47, 33, 11, 7
};
 
const float Values::scTable[] = {
  1.1, 2.2, 3.3, 4.4
};
 
const char Values::scLetters[] = {
  'a', 'b', 'c', 'd', 'e',
  'f', 'g', 'h', 'i', 'j'
};
 
float Values::table[4] = {
  1.1, 2.2, 3.3, 4.4
};
 
char Values::letters[10] = {
  'a', 'b', 'c', 'd', 'e',
  'f', 'g', 'h', 'i', 'j'
};
 
int main() { Values v; } ///:~

Avec les static const de types intégraux vous pouvez fournir la définition dans la classe, mais pour tout le reste (y compris des tableaux de types intégraux, même s'ils sont static const) vous devez fournir une définition unique pour le membre. Ces définitions ont des conventions de liens internes, si bien qu'elles peuvent être placées dans les fichiers d'en-tête. La syntaxe pour initialiser les tableaux statiques est la même que pour n'importe quel agrégat, y compris le comptage automatique.

Vous pouvez aussi créer des objets static const d'un type de classe et des tableaux de tels objets. Toutefois, vous ne pouvez pas les initialiser en utilisant la “syntaxe inline” autorisée pour les staticconst de type prédéfinis intégraux :

 
Sélectionnez
//: C10:StaticObjectArrays.cpp
// Tableaux statiques d'objets d'une classe
class X {
  int i;
public:
  X(int ii) : i(ii) {}
};
 
class Stat {
  // Ceci ne marche pas, bien que
  // vous puissiez en avoir envie :
//!  static const X x(100);
  // Les objets statiques de type const et non-const 
  // doivent être initialisés à l'extérieur :
  static X x2;
  static X xTable2[];
  static const X x3;
  static const X xTable3[];
};
 
X Stat::x2(100);
 
X Stat::xTable2[] = {
  X(1), X(2), X(3), X(4)
};
 
const X Stat::x3(100);
 
const X Stat::xTable3[] = {
  X(1), X(2), X(3), X(4)
};
 
int main() { Stat v; } ///:~

L'initialisation de tableaux d'objets const et non conststatic doit être réalisée de la même façon, suivant la syntaxe typique de la définition static.

XI-C-2. Classes imbriquées et locales

Vous pouvez facilement placer des données membres statiques dans des classes imbriquées au sein d'autres classes. La définition de tels membres est une extension intuitive et évidente – vous utilisez simplement un niveau supplémentaire de résolution de portée. Toutefois, vous ne pouvez pas avoir de données membres static dans des classes locales (une classe locale est une classe définie dans une fonction). Ainsi,

 
Sélectionnez
//: C10:Local.cpp
// Membres statiques & classes locales
#include <iostream>
using namespace std;
 
// Une classe imbriquée PEUT avoir des données membres statiques :
class Outer {
  class Inner {
    static int i; // OK
  };
};
 
int Outer::Inner::i = 47;
 
// Une classe locale ne peut pas avoir de donné membre statique :
void f() {
  class Local {
  public:
//! static int i;  // Erreur
    // (Comment définiriez-vous i?)
  } x;
} 
 
int main() { Outer x; f(); } ///:~

Vous pouvez voir le problème immédiat avec un membre static dans une classe locale : Comment décrivez-vous la donnée membre à portée de fichier afin de la définir ? En pratique, les classes locales sont très rarement utilisées.

XI-C-3. Fonctions membres statiques

Vous pouvez aussi créer des fonctions membres static qui, comme les données membres static, travaille pour la classe comme un tout plutôt que pour un objet particulier de la classe. Au lieu de créer une fonction globale qui vit dans et “pollue” l'espace de nommage global ou local, vous portez la fonction dans la classe. Quand vous créez une fonction membre static, vous exprimez une association avec une classe particulière.

Vous pouvez appeler une fonction membre static de manière ordinaire, avec le point ou la flèche, en association avec un objet. Toutefois, il est plus courant d'appeler une fonction membre static elle-même, sans objet spécifique, en utilisant l'opérateur de résolution de portée comme ceci :

 
Sélectionnez
//: C10:SimpleStaticMemberFunction.cpp 
class X {
public:
  static void f(){};
};
 
int main() {
  X::f();
} ///:~

Quand vous voyez des fonctions membres statiques dans une classe, souvenez-vous que le concepteur a voulu que cette fonction soit conceptuellement associée avec la classe dans son ensemble.

Une fonction membre static ne peut pas accéder aux données membres ordinaires, mais uniquement aux données membres static. Elle peut appeler uniquement d'autres fonctions membres static. Normalement, l'adresse de l'objet courant ( this) est calmement passée quand n'importe quelle fonction membre est appelée, mais un membre static n'a pas de this, ce qui explique pourquoi il ne peut accéder aux membres ordinaires. Ainsi, vous profitez de la légère augmentation de vitesse que peut fournir une fonction globale parce qu'une fonction membre static n'a pas besoin du temps système supplémentaire nécessaire pour passer this. En même temps vous avez les bénéfices du fait d'avoir la fonction dans la classe.

Pour les données membres, static indique qu'un seul espace de stockage existe pour tous les objets d'une classe. Ceci est analogue à l'utilisation de static pour définir les objets dans une fonction pour signifier qu'une seule copie d'une variable locale est utilisée pour tous les appels de cette fonction.

Voici un exemple montrant un usage simultané de données et fonctions membres static:

 
Sélectionnez
//: C10:StaticMemberFunctions.cpp
class X {
  int i;
  static int j;
public:
  X(int ii = 0) : i(ii) {
     // Une fonction membre non statique peut accéder aux
     // fonctions ou données membres statiques :
    j = i;
  }
  int val() const { return i; }
  static int incr() {
    //! i++; // Erreur : les fonctions membres statiques
    // ne peuvent accéder aux données membres non statiques
    return ++j;
  }
  static int f() {
    //! val(); // Erreur : les fonctions membres statiques
    // ne peuvent accéder aux fonctions membres non statiques 
    return incr(); // OK -- appelle une statique
  }
};
 
int X::j = 0;
 
int main() {
  X x;
  X* xp = &x;
  x.f();
  xp->f();
  X::f(); // Ne fonctionne qu'avec les membres statiques
} ///:~

Parce qu'elles n'ont pas de pointeur this, les fonctions membres static ne peuvent ni accéder aux données membres non static, ni appeler des fonctions membres non static.

Remarquez dans main( ) qu'un membre static peut être sélectionné en utilisant la syntaxe habituelle (point ou flèche), associant cette fonction à un objet, mais également avec aucun objet (parce qu'un membre static est associé avec une classe, pas un objet particulier), en utilisant le nom de la classe et l'opérateur de résolution de portée.

Voici un aspect intéressant : à cause de la façon dont l'initialisation a lieu pour les objets membres static, vous pouvez placer une donnée membre static de la même classe au sein de cette classe. Voici un exemple qui n'autorise l'existence que d'un seul objet de type Egg( Oeuf, ndt) en rendant le constructeur privé. Vous pouvez accéder à cet objet, mais vous ne pouvez pas créer de nouvel objet Egg:

 
Sélectionnez
//: C10:Singleton.cpp
// Membres statiques de même type, garantissent que
// seulement un objet de ce type existe.
// Egalement dénommés technique "singleton".
#include <iostream>
using namespace std;
 
class Egg {
  static Egg e;
  int i;
  Egg(int ii) : i(ii) {}
  Egg(const Egg&); // Evite la copie de construction
public:
  static Egg* instance() { return &e; }
  int val() const { return i; }
};
 
Egg Egg::e(47);
 
int main() {
//!  Egg x(1); // Erreur -- ne peut créer un Egg
  // Vous pouvez accéder à la seule instance :
  cout << Egg::instance()->val() << endl;
} ///:~

L'initialisation de E a lieu après que la déclaration de la classe soit terminée, si bien que le compilateur a toutes les informations dont il a besoin pour allouer le stockage et faire appel au constructeur.

Pour éviter complètement la création de tout autre objet, quelque chose d'autre a été ajouté : un deuxième constructeur privé appelé constructeur-copie. À ce point du livre, vous ne pouvez pas savoir pourquoi c'est nécessaire du fait que le constructeur-copie ne sera pas introduit avant le prochain chapitre. Toutefois, comme premier aperçu, si vous supprimiez le constructeur-copie défini dans l'exemple ci-dessus, vous seriez capable de créer un objet Egg comme ceci :

 
Sélectionnez
Egg e = *Egg::instance();
Egg e2(*Egg::instance());

Ces deux codes utilisent le constructeur-copie, afin d'éviter cette possibilité le constructeur-copie est déclaré privé (aucune définition n'est nécessaire parce qu'il n'est jamais appelé). Une grande partie du chapitre suivant est consacrée au constructeur-copie ce qui vous permettra de mieux comprendre ce dont il est question ici.

XI-D. Dépendance de l'initialisation statique

Au sein d'une unité de traduction spécifique, il est garanti que l'ordre d'initialisation des objets statiques sera le même que celui dans lequel les définitions des objets apparaissent dans cette unité de traduction. L'ordre de destruction aura la garantie de s'effectuer dans le sens inverse de l'initialisation.

Toutefois, il n'y a aucune garantie concernant l'ordre d'initialisation des objets statiques entre unités de traduction, et le langage ne fournit aucun moyen de spécifier cet ordre. Ceci peut être la source d'importants problèmes. Voici un exemple de désastre immédiat (qui arrêtera les systèmes d'exploitation primitifs et tuera le processus sur ceux qui sont plus sophistiqués), si un fichier contient :

 
Sélectionnez
//: C10:Out.cpp {O}
// Premier fichier
#include <fstream>
std::ofstream out("out.txt"); ///:~

et un autre fichier utilise l'objet out dans un de ses initialisateurs

 
Sélectionnez
//: C10:Oof.cpp
// Deuxième fichier
//{L} Out
#include <fstream>
extern std::ofstream out;
class Oof {
public:
  Oof() { std::out << "oups"; }
} oof;
 
Sélectionnez
int main() {} ///:~

le programme peut marcher ou ne pas marcher. Si l'environnement de programmation construit le programme de telle sorte que le premier fichier est initialisé avant le second, alors il n'y a aucun problème. Toutefois, si le second est initialisé avant le premier, le constructeur de Oof dépend de l'existence de out, qui n'a pas encore été construit ce qui produit une situation chaotique.

Ce problème ne se produit qu'avec les initialisations d'objets statiques qui dépendent les uns des autres. Les statiques dans une unité de traduction sont initialisés avant le premier appel à une fonction dans cette unité – mais ce peut être après main( ). Vous ne pouvez avoir de certitude sur l'ordre d'initialisation d'objets statiques s'ils se trouvent dans différents fichiers.

Un exemple plus subtil se trouve dans l'ARM. (46)Dans un fichier vous avez, à portée globale :

 
Sélectionnez
extern int y;
int x = y + 1;

et dans un second fichier, vous avez, à portée globale également :

 
Sélectionnez
extern int x;
int y = x + 1;

Pour tous les objets statiques, le mécanisme d'édition de liens et de chargement garantit une initialisation statique à zéro avant que l'initialisation dynamique spécifiée par le programmeur ait lieu. Dans l'exemple précédent, la mise à zéro de l'espace de stockage occupé par l'objet fstream out n'a pas de signification spéciale, si bien qu'il est réellement indéfini jusqu'à ce que le constructeur soit appelé. Toutefois, avec les types prédéfinis, l'initialisation à zéro a réellement un sens, et si les fichiers sont initialisés dans l'ordre montré ci-dessus, y commence initialisé statiquement à zéro, si bien que x prend la valeur un, et y est dynamiquement initialisé à deux. Toutefois, si les fichiers sont initialisés dans l'ordre inverse, x est statiquement initialisé à zéro, y est dynamiquement initialisé à un, et x prend alors la valeur deux.

Les programmeurs doivent en être conscients parce qu'ils peuvent créer un programme avec des dépendances à l'initialisation statique qui fonctionne sur une plateforme, mais qui, après avoir été déplacé dans un autre environnement de compilation, cesse brutalement et mystérieusement de fonctionner.

XI-D-1. Que faire

Il y a trois approches pour traiter ce problème :

  1. Ne le faites pas. Éviter les dépendances à l'initialisation statique est la meilleure solution.
  2. Si vous devez le faire, placez les définitions d'objets statiques critiques dans un fichier unique, afin que vous puissiez contrôler de manière portable leur initialisation en les plaçant dans le bon ordre.
  3. Si vous êtes convaincus qu'il est inévitable de disperser des objets statiques dans vos unités de traduction – comme dans le cas d'une bibliothèque de fonctions, où vous ne pouvez contrôler le programmeur qui l'utilise – il y a deux techniques de programmation pour résoudre ce problème.

Première technique

Le pionnier de cette technique est Jerry Schwarz, alors qu'il créait la bibliothèque iostream (parce que les définitions de cin, cout, et cerr sont static et se trouvent dans un fichier différent). Elle est en fait inférieure à la deuxième technique, mais elle est utilisée depuis un moment et vous pourriez donc croiser du code qui l'utilise ; il est donc important que vous compreniez comment elle fonctionne.

Cette technique requiert une classe supplémentaire dans votre fichier d'en-tête. Cette classe est responsable de l'initialisation dynamique des objets statiques de votre bibliothèque. Voici un exemple simple :

 
Sélectionnez
//: C10:Initializer.h
// Technique d'initialisation statique 
#ifndef INITIALIZER_H
#define INITIALIZER_H
#include <iostream>
extern int x; // Déclarations, non pas définitions
extern int y;
 
class Initializer {
  static int initCount;
public:
  Initializer() {
    std::cout << "Initializer()" << std::endl;
    // Initialise la première fois seulement
    if(initCount++ == 0) {
      std::cout << "performing initialization"
                << std::endl;
      x = 100;
      y = 200;
    }
  }
  ~Initializer() {
    std::cout << "~Initializer()" << std::endl;
    // Clean up last time only
    if(--initCount == 0) {
      std::cout << "performing cleanup" 
                << std::endl;
      // Tout nettoyage nécessaire ici
    }
  }
};
 
// Ce qui suit crée un objet dans chaque
// fichier où Initializer.h est inclus, mais cet
// objet n'est visible que dans ce fichier :
static Initializer init;
#endif // INITIALIZER_H ///:~

Les déclarations de x et y annoncent seulement que ces objets existent, mais elles n'allouent pas d'espace de stockage pour les objets. Toutefois, la définition pour le Initializer init alloue l'espace pour cet objet dans tous les fichiers où le fichier d'en-tête est inclus. Mais parce que le nom est static(contrôlant cette fois la visibilité, pas la façon dont le stockage est alloué ; le stockage est à portée de fichier par défaut), il est visible uniquement au sein de cette unité de traduction, si bien que l'éditeur de liens ne se plaindra pas au sujet d'erreurs liées à des définitions multiples.

Voici le fichier contenant les définitions pour x, y et initCount:

 
Sélectionnez
//: C10:InitializerDefs.cpp {O}
// Définitions pour Initializer.h
#include "Initializer.h"
// L'initialisation statique forcera
// toutes ces valeurs à zéro :
int x;
int y;
int Initializer::initCount;
///:~

(Bien sûr, une instance statique fichier d' init est également placée dans ce fichier quand le fichier d'en-tête est inclu.) Suppposez que deux autres fichiers soient créés par l'utilisateur de la bibliothèque :

 
Sélectionnez
//: C10:Initializer.cpp {O}
// Initialisation statique
#include "Initializer.h"
///:~

and

 
Sélectionnez
//: C10:Initializer2.cpp
//{L} InitializerDefs Initializer
// Initialisation statique
#include "Initializer.h"
using namespace std;
 
int main() {
  cout << "inside main()" << endl;
  cout << "leaving main()" << endl;
} ///:~

Maintenant, l'ordre d'initialisation des unités de traduction n'a plus d'importance. La première fois qu'une unité de traduction contenant Initializer.h est initialisée, initCount vaudra zéro si bien que l'initialisation sera réalisée. (Ceci dépend fortement du fait que l'espace de stockage statique est fixé à zéro avant que l'initialisation dynamique n'ait lieu.) Pour le reste des unités de traduction, initCount ne vaudra pas zéro et l'initialisation sera omise. Le nettoyage se déroule dans l'ordre inverse, et ~Initializer( ) garantit qu'il ne se produira qu'une fois.

Cet exemple a utilisé des types prédéfinis comme les objets statiques globaux. La technique fonctionne aussi avec les classes, mais ces objets doivent alors être initialisés dynamiquement par la classe Initializer. Une façon de le faire est de créer les classes sans constructeurs ni destructeurs, mais à la place avec des fonctions initialisation et nettoyage utilisant des noms différents. Une approche plus habituelle, toutefois, est d'avoir des pointeurs vers des objets et de les créer en utilisant new dans Initializer( ).

Deuxième technique

Longtemps après que la première technique ait été utilisée, quelqu'un (je ne sais pas qui) a trouvé la technique expliquée dans cette section, qui est beaucoup plus simple et propre que la première. Le fait qu'il ait fallu si longtemps pour la découvrir est un hommage à la complexité du C++.

Cette technique repose sur le fait que les objets statiques dans les fonctions sont initialisés la première fois (seulement) que la fonction est appelée. Gardez à l'esprit que le problème que nous essayons de résoudre ici n'est pas quand les objets statiques sont initialisés (ceci peut être contrôlé séparément), mais plutôt s'assurer que l'initialisation se déroule correctement.

Cette technique est très claire et très ingénieuse. Quelle que soit la dépendance d'initialisation, vous placez un objet statique dans une fonction qui renvoie une référence vers cet objet. Ainsi, la seule façon d'accéder à cet objet statique est d'appeler la fonction, et si cet objet a besoin d'accéder à d'autres objets statiques dont il dépend il doit appeler leur fonction. Et la première fois qu'une fonction est appelée, cela force l'initialisation. L'ordre correct d'initialisation statique est garanti par la structure du code, pas par un ordre arbitraire établi par l'éditeur de liens.

Pour construire un exemple, voici deux classes qui dépendent l'une de l'autre. La première contient un bool qui est initialisé uniquement par le constructeur, si bien que vous pouvez dire si le constructeur a été appelé pour une instance statique de la classe (l'aire de stockage statique est initialisée à zéro au démarrage du programme, ce qui produit une valeur false pour le bool si le constructeur n'a pas été appelé) :

 
Sélectionnez
//: C10:Dependency1.h
#ifndef DEPENDENCY1_H
#define DEPENDENCY1_H
#include <iostream>
 
class Dependency1 {
  bool init;
public:
  Dependency1() : init(true) {
    std::cout << "Dependency1 construction" 
              << std::endl;
  }
  void print() const {
    std::cout << "Dependency1 init: " 
              << init << std::endl;
  }
};
#endif // DEPENDENCY1_H ///:~

Le constructeur signale aussi quand il est appelé, et vous pouvez afficher ( print( )) l'état de l'objet pour voir s'il a été initialisé.

La deuxième classe est initialisée à partir d'un objet de la première, ce qui causera la dépendance:

 
Sélectionnez
//: C10:Dependency2.h
#ifndef DEPENDENCY2_H
#define DEPENDENCY2_H
#include "Dependency1.h"
 
class Dependency2 {
  Dependency1 d1;
public:
  Dependency2(const Dependency1& dep1): d1(dep1){
    std::cout << "Dependency2 construction ";
    print();
  }
  void print() const { d1.print(); }
};
#endif // DEPENDENCY2_H ///:~

Le constructeur s'annonce et affiche l'état de l'objet d1 afin que vous puissiez voir s'il a été initialisé au moment où le constructeur est appelé.

Pour montrer ce qui peut mal se passer, le fichier suivant commence par mettre les définitions statiques dans le mauvais ordre, comme elles se produiraient si l'éditeur de liens initialisait l'objet Dependency2 avant l'objet Dependency1. Puis l'ordre est inversé pour montrer comment il fonctionne correctement si l'ordre se trouve être le “bon”. Finalement, la deuxième technique est démontrée.

Pour fournir une sortie plus lisible, la fonction separator( ) est créée. Le truc est que vous ne pouvez pas appeler une fonction globalement à moins que cette fonction ne soit utilisée pour réaliser l'initialisation de la variable, si bien que separator( ) renvoie une valeur factice qui est utilisée pour initialiser deux variables globales.

 
Sélectionnez
//: C10:Technique2.cpp
#include "Dependency2.h"
using namespace std;
 
// Renvoie une valeur si bien qu'elle peut être appelée comme
// un initialiseur global :
int separator() {
  cout << "---------------------" << endl;
  return 1;
}
 
// Simule le problème de dépendance :
extern Dependency1 dep1;
Dependency2 dep2(dep1);
Dependency1 dep1;
int x1 = separator();
 
//, Mais s'il se produit dans cet ordre, cela marche bien :
Dependency1 dep1b;
Dependency2 dep2b(dep1b);
int x2 = separator();
 
// Englober les objets statiques dans des fonctions marche bien
Dependency1& d1() {
  static Dependency1 dep1;
  return dep1;
}
 
Dependency2& d2() {
  static Dependency2 dep2(d1());
  return dep2;
}
 
int main() {
  Dependency2& dep2 = d2();
} ///:~

Les fonctions d1( ) et d2( ) englobent les instances statiques des objets Dependency1 et Dependency2. À présent, la seule façon d'accéder aux objets statiques est d'appeler les fonctions et cela force l'initialisation statique lors du premier appel de la fonction. Ceci signifie que l'initialisation est garantie correcte, ce que vous verrez lorsque vous exécuterez le programme et regarderez la sortie.

Voici comment vous organiseriez réellement le code pour utiliser la technique. Normalement, les objets statiques seraient définis dans des fichiers différents (parce que vous y êtes forcés pour une raison quelconque; souvenez-vous que définir les objets statiques dans différents fichiers est la cause du problème), et à la place vous définissez les fonctions englobantes dans des fichiers séparés. Mais elles devront être déclarées dans des fichiers d'en-tête :

 
Sélectionnez
//: C10:Dependency1StatFun.h
#ifndef DEPENDENCY1STATFUN_H
#define DEPENDENCY1STATFUN_H
#include "Dependency1.h"
extern Dependency1& d1();
#endif // DEPENDENCY1STATFUN_H ///:~

En fait, le “extern” est redondant pour la déclaration de fonction. Voici le deuxième fichier d'en-tête :

 
Sélectionnez
//: C10:Dependency2StatFun.h
#ifndef DEPENDENCY2STATFUN_H
#define DEPENDENCY2STATFUN_H
#include "Dependency2.h"
extern Dependency2& d2();
#endif // DEPENDENCY2STATFUN_H ///:~

Dans les fichiers d'implémentation où vous auriez auparavant placé les définitions des objets statiques, vous mettez maintenant à la place les définitions des fonctions englobantes :

 
Sélectionnez
//: C10:Dependency1StatFun.cpp {O}
#include "Dependency1StatFun.h"
Dependency1& d1() {
  static Dependency1 dep1;
  return dep1;
} ///:~

A priori, du code supplémentaire peut également être placé dans ces fichiers. Voici l'autre fichier :

 
Sélectionnez
//: C10:Dependency2StatFun.cpp {O}
#include "Dependency1StatFun.h"
#include "Dependency2StatFun.h"
Dependency2& d2() {
  static Dependency2 dep2(d1());
  return dep2;
} ///:~

Ainsi, il y a maintenant deux fichiers qui pourraient être liés dans n'importe quel ordre et s'ils contenaient des objets statiques ordinaires pourraient supporter n'importe quel ordre d'initialisation. Mais comme ils contiennent les fonctions englobantes, il n'y a aucun risque d'initialisation impropre :

 
Sélectionnez
//: C10:Technique2b.cpp
//{L} Dependency1StatFun Dependency2StatFun
#include "Dependency2StatFun.h"
int main() { d2(); } ///:~

Quand vous exécuterez ce programme, vous verrez que l'initialisation de l'objet statique Dependency1 a toujours lieu avant l'initialisation de l'objet statique Dependency2. Vous pouvez aussi constater que c'est une approche beaucoup plus simple que la première technique.

Vous pourriez être tentés d'écrire d1( ) et d2( ) comme des fonctions inline dans leur fichier d'en-tête respectif, mais c'est quelque chose qu'il faut absolument éviter. Une fonction inline peut être dupliquée dans tous les fichiers où elle apparaît – et cette duplication inclut la définition des objets statiques. Comme les fonctions inline sont automatiquement par défaut en convention de liaison interne, ceci entrainerait la présence de plusieurs objets statiques parmi les différentes unités de traduction, ce qui causerait certainement des problèmes. Vous devez donc vous assurer qu'il n'y a qu'une seule définition de chaque fonction englobante, et ceci veut dire qu'il ne faut pas les rendre inline.

XI-E. Spécification alternative des conventions de liens

Que se passe-t-ilsi vous écrivez un programme en C++ et que vous voulez utiliser une bibliothèque de fonctions C ? Si vous faites la déclaration de fonction C,

 
Sélectionnez
float f(int a, char b);

le compilateur C++ décorera ce nom en quelque chose comme _f_int_char pour supporter la surcharge de fonction (et la convention de liens sécurisée au niveau des types). Toutefois, le compilateur C qui a compilé votre bibliothèque C n'a plus que probablement pas décoré le nom, si bien que son nom interne sera _f. Ainsi, l'éditeur de liens ne sera pas capable de résoudre vos appels en C++ à f( ).

Le mécanisme d'échappement fourni en C++ est la spécification alternative des conventions de liens, qui a été produite dans le langage en surchargeant le mot-clé extern. extern est suivi par une chaine qui spécifie la convention de liens que vous désirez pour la déclaration, suivie par la déclaration :

 
Sélectionnez
extern "C" float f(int a, char b);

Ceci dit au compilateur de fournir une convention de lien C à f( ) afin que le compilateur ne décore pas le nom. Les deux seuls types de spécification de convention de liens supportés par le standard sont “C” et “C++,”, mais les vendeurs de compilateurs ont la possibilité de supporter d'autres langages de la même façon.

Si vous avez un groupe de déclarations avec une convention de liens alternative, mettez-les entre des accolades, comme ceci :

 
Sélectionnez
extern "C" {
  float f(int a, char b);
  double d(int a, char b);
}

Ou, pour un fichier d'en-tête,

 
Sélectionnez
extern "C" {
#include "Myheader.h"
}

La plupart des vendeurs de compilateurs C++ gèrent la spécification de convention de liens alternative dans leurs fichiers d'en-tête qui fonctionnent à la fois en C et en C++, et vous n'avez donc pas à vous en inquiéter.

XI-F. Sommaire

Le mot-clé static peut être confus parce que dans beaucoup de situations, il contrôle la position du stockage, et dans d'autres cas il contrôle la visibilité et le lien des noms.

Avec l'introduction des espaces de noms (namespaces) en C++, vous avez une alternative améliorée et plus flexible pour commander la prolifération des noms dans de grands projets.

L'utilisation de static à l'intérieur des classes est une possibilité supplémentaire de contrôler les noms dans un programme. Les noms ne peuvent pas être en conflit avec des noms globaux, et la visibilité et l'accès est gardé dans le programme, donnant un meilleur contrôle dans l'entretien de votre code.

XI-G. Exercices

La solution de certains exercices sélectionnés peut se trouver dans le document électronique The Thinking in C++ Annotated Solution Guide, disponible pour une somme modique à www.BruceEckel.com.

  1. Créez une fonction avec une variable statique qui est un pointeur (avec un argument par défaut à zéro). Quand l'appelant fournit une valeur pour cet argument, elle est utilisée pour pointer au début d'un tableau de int. Si vous appelez la fonction avec un argument à zéro (utilisant l'argument par défaut), la fonction retourne la prochaine valeur dans le tableau, jusqu'à ce qu'il voit une valeur “-1” dans le tableau (pour agir comme indicateur de fin de tableau). Testez cette fonction dans le main( ).
  2. Créez une fonction qui retourne la prochaine valeur d'une séquence de Fibonacci à chaque fois que vous l'appelez. Ajoutez un argument de type bool avec une valeur par défaut à false tel que quand vous donnez l'argument à true il “réinitialise” la fonction au début de la séquence de Fibonacci. Testez cette fonction dans le main( ).
  3. Créez une classe avec un tableau de int s. Définissez la taille du tableau en utilisant static const int dans la classe. Ajoutez une variable const int, et initialisez la dans la liste d'initialisation du constructeur ; rendez le constructeur inline. Ajoutez un membre static int et initialisez-le avec une valeur spécifique. Ajoutez une fonction membre static qui affiche la donnée du membre static. Ajoutez une fonction membre inline appelant print( ) pour afficher sur la sortie toutes les valeurs du tableau et pour appeler la fonction membre static. Testez cette fonction dans le main( ).
  4. Créez une classe appelée Monitor qui garde la trace du nombre de fois qu'une fonction membre incident( ) a été appelée. Ajoutez une fonction membre print( ) qui affiche le nombre d'incidents. Maintenant créez une fonction globale (pas une fonction membre) contenant un objet static Monitor. Chaque fois que vous appelez cette fonction, elle appellera incident( ),puis print( ) pour afficher le compteur d'incidents. Testez cette fonction dans le main( ).
  5. Modifiez la classe Monitor de l'exercice 4 pour que vous puissiez décrémenter ( decrement( )) le compteur d'incidents. Fabriquez une classe Monitor2 qui prend en argument du constructeur un pointeur sur un Monitor1, et qui stocke ce pointeur et appelle incident( ) et print( ). Dans le destructeur de Monitor2, appelez decrement( ) et print( ). Maintenant fabriquez un objet staticMonitor2 dans une fonction. À l'intérieur du main( ), expérimentez en appelant et en n'appelant pas la fonction pour voir ce qui arrive avec le destructeur de Monitor2.
  6. Fabriquez un objet global Monitor2 et voyez ce qui arrive.
  7. Créez une classe avec un destructeur qui affiche un message et qui appelle exit( ). Créez un objet global de cette classe et regardez ce qui arrive.
  8. Dans StaticDestructors.cpp, expérimentez avec l'ordre d'appel des constructeurs et destructeurs en appelant f( ) et g( ) dans le main( ) dans des ordres différents. Votre compilateur le fait-il correctement ?
  9. Dans StaticDestructors.cpp, testez la gestion des erreurs par défaut de votre implémentation en changeant la définition originale de out en une déclaration extern et mettez la définition actuelle après la définition de a(à qui le constructeur d' Obj envoie des informations à out). Assurez-vous qu'il n'y ait rien d'important qui tourne sur votre machine quand vous lancez le programme ou alors que votre machine gère solidement les plantages.
  10. Prouvez que les variables statique de fichier dans un fichier d'en-tête ne sont pas en désaccord les unes avec les autres quand elles sont incluses dans plus d'un fichier cpp.
  11. Créez une classe simple contenant un int, un constructeur qui initialise le int depuis son argument, une fonction membre pour affecter l' int avec son argument, et une fonction print( ) qui affiche ce int. Mettez votre classe dans un fichier d'en-tête, et incluez le fichier d'en-tête dans deux fichiers cpp. Dans un fichier cpp faites une instance de votre classe, et dans l'autre déclarez un identifiant extern et testez-le dans le main( ). Rappelez vous que vous devez lier les deux fichiers objet ou sinon le linker ne pourra pas trouver l'objet.
  12. Rendez statique l'instance de l'objet de l'exercice 11 et vérifiez que il ne peut pas être trouvé par le linker pour cette raison.
  13. Déclarez une fonction dans un fichier d'en-tête. Définissez la fonction dans un fichier cpp et appelez-la dans le main( ) dans un second fichier cpp. Compilez et vérifiez que ça marche. Maintenant changez la définition de la fonction de façon à la rendre static et vérifiez que le linker ne peut pas le trouver.
  14. Modifiez Volatile.cpp du chapitre 8 pour faire de comm::isr( ) quelque chose qui pourrait en réalité marcher comme une routine de service d'interruption. Conseil : une routine de service d'interruption ne prend aucun argument.
  15. Écrivez et compilez un programme simple qui utilise les mots-clés auto et register.
  16. Créez un fichier d'en-tête contenant un namespace. À l'intérieur de ce namespace créez plusieurs déclarations de fonctions. Maintenant, créez un second fichier d'en-tête qui inclut le premier et qui continue le namespace, en ajoutant plusieurs autres déclarations de fonctions. Maintenant, créez un fichier cpp qui inclut le second fichier d'en-tête. Renommez votre namespace avec un autre diminutif (plus petit). Dans la définition d'une fonction, appelez une de vos fonctions utilisant la résolution de portée. Dans une définition de fonction séparée, écrivez une directive using permettant d'insérer votre namespace dans la portée de cette fonction, et montrez que vous n'avez pas besoin de résolution de portée pour appeler la fonction depuis votre namespace.
  17. Créez un fichier d'en-tête avec un namespace anonyme. Inclure l'en-tête dans deux fichiers cpp séparés et montrez qu’un espace anonyme est unique pour chaque unité de traduction.
  18. En utilisant le fichier d'en-tête de l'exercice 17, montrez que les noms dans le namespace anonyme sont automatiquement disponibles dans l'unité de traduction sans les qualifier.
  19. Modifiez FriendInjection.cpp pour ajouter une définition pour la fonction amie et appelez la fonction dans le main( ).
  20. Dans Arithmetic.cpp, démontrez que la directive using ne s'étend pas en dehors de la portée la fonction dans laquelle la directive fut créée.
  21. Réparez le problème dans OverridingAmbiguity.cpp, d'abord avec la résolution de portée, puis à la place de cela avec une déclaration using qui force le compilateur à choisir l'une des fonctions ayant des noms identiques.
  22. Dans deux fichiers d'en-tête, créez deux namespaces, chaque un contenant une classe (avec toutes les définitions inline) avec un nom identique à celui dans l'autre namespace. Créez un fichier cpp qui inclut les deux fichiers d'en-tête. Créez une fonction, et à l'intérieur de la fonction utilisez la directive using pour introduire les deux namespace. Essayez de créer un objet de la classe et de voir ce qui arrive. Rendez la directive using globale (en dehors de la fonction) pour voir si cela fait une différence. Réparez le problème utilisant la résolution de portée, et créez des objets de chaque classe.
  23. Réparez le problème de l'exercice 22 avec une déclaration using qui force le compilateur à choisir un nom des classes identiques.
  24. Extrayez la déclaration de namespace dans BobsSuperDuperLibrary.cpp et UnnamedNamespaces.cpp et mettez-les dans des fichiers d'en-tête séparés, en donnant un nom au namespace anonyme dans ce processus. Dans un troisième fichier d'en-tête créez un nouveau namespace qui combine les éléments des deux autres namespace avec des déclarations using. Dans le main( ), introduisez votre nouveau namespace avec une directive using et accédez à tous les éléments de votre namespace.
  25. Créez un fichier d'en-tête qui inclut <string> et <iostream>, mais sans utiliser aucune directive using ou déclaration using. Ajoutez les “gardes d'inclusion” comme vous l'avez vu dans le chapitre sur les fichiers d'en-tête dans ce livre. Créez une classe avec toutes ses fonctions inline qui contient un membre string, avec un constructeur qui initialise ce string depuis son argument et une fonction print( ) qui affiche le string. Créez un fichier cpp et testez votre classe dans le main( ).
  26. Créez une classe contenant un static double et long. Écrivez une fonction membre static qui affiche les valeurs.
  27. Créez une classe contenant un int, un constructeur qui initialise le int depuis son argument, et une fonction print( ) pour afficher le int. Maintenant créez une seconde classe qui contient un objet static de la première. Ajoutez une fonction membre static qui appelle la fonction print( ) de l'objet static. Testez votre classe dans le main( ).
  28. Créez une classe contenant deux tableaux static de int l'un const et l'autre non- const. Écrivez des méthodes static pour afficher les tableaux. Testez votre classe dans le main( ).
  29. Créez une class contenant une string, avec un constructeur qui initialise la string depuis son argument, et une fonction print( ) pour afficher la string. Créez une autre classe qui contient deux tableaux d'objet staticconst et non- const de la première classe, et des méthodes static pour afficher ces tableaux. Testez cette seconde classe dans le main( ).
  30. Créez une struct qui contient un int et un constructeur par défaut qui initialise le int à zéro. Rendez cette struct locale à une fonction. À l'intérieur de cette fonction, créez un tableau d'objets de votre struct et démontrez que chaque int dans le tableau a automatiquement était initialisé à zéro.
  31. Créez une classe qui représente une connexion d'imprimante, et qui ne vous autorise qu'une seule imprimante.
  32. Dans un fichier d'en-tête, créez une classe Mirror qui contient deux données membre : un pointeur sur un objet Mirror et un bool. Donnez-lui deux constructeurs : le constructeur par défaut initialise le bool à true et le pointeur de Mirror à zéro. Le second constructeur prend en argument un pointeur sur un objet Mirror, qu'il affecte au pointeur interne de l'objet ; il met le bool à false. Ajoutez une fonction membre test( ): si le pointeur de l'objet n'est pas zéro, il retourne la valeur de test( ) appelé à travers le pointeur. Si le pointeur est zéro, il retourne le bool. Maintenant, créez cinq fichiers cpp, chacun incluant le fichier d'en-tête de Mirror. Le premier fichier cpp définit un objet global Mirror utilisant le constructeur par défaut. Le second fichier déclare l'objet du premier fichier comme extern, et définit un objet global Mirror utilisant le second constructeur, avec un pointeur sur le premier objet. Continuez à faire cela jusqu'au dernier fichier, qui contiendra aussi une définition d'objet global. Dans ce fichier, main( ) devrait appeler la fonction test( ) et reporter le résultat. Si le résultat est true, trouvez comment changer l'ordre des liens sur votre linker et changez-le jusqu'à ce que le résultat soit false.
  33. Réparez le problème de l'exercice 32 en utilisant la technique numéro un montrée dans ce livre.
  34. Réparez le problème de l'exercice 32 en utilisant la technique numéro deux montrée dans ce livre.
  35. Sans inclure un fichier d'en-tête, déclarez la fonction puts( ) depuis la librairie standard C. Appelez cette fonction depuis le main( ).

précédentsommairesuivant
Bjarne Stroustrup et Margaret Ellis, The Annotated C++ Reference Manual, Addison-Wesley, 1990, pp. 20-21.

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.