web-dev-qa-db-fra.com

Pourquoi les membres de données statiques doivent être définis en dehors de la classe séparément en C ++ (contrairement à Java)?

class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

Je ne vois pas la nécessité d'avoir A::x défini séparément dans un fichier .cpp (ou même fichier pour les modèles). Pourquoi ne peut pas être A::x déclaré et défini en même temps?

A-t-il été interdit pour des raisons historiques?

Ma question principale est: cela affectera-t-il toute fonctionnalité si static membres de données ont été déclarés/définis en même temps (comme Java)?

41
iammilind

Je pense que la limitation que vous avez envisagée n'est pas liée à la sémantique (pourquoi quelque chose devrait-il changer si l'initialisation était définie dans le même fichier?) Mais plutôt au modèle de compilation C++ qui, pour des raisons de compatibilité descendante, ne peut pas être facilement changé car il soit devenir trop complexe (prendre en charge un nouveau modèle de compilation et le modèle existant en même temps) ou ne pas permettre de compiler le code existant (en introduisant un nouveau modèle de compilation et en supprimant le modèle existant).

Le modèle de compilation C++ découle de celui de C, dans lequel vous importez des déclarations dans un fichier source en incluant des fichiers (en-tête). De cette façon, le compilateur voit exactement un gros fichier source, contenant tous les fichiers inclus, et tous les fichiers inclus à partir de ces fichiers, récursivement. Cela présente un gros avantage pour l'OMI, à savoir qu'il facilite la mise en œuvre du compilateur. Bien sûr, vous pouvez écrire n'importe quoi dans les fichiers inclus, c'est-à-dire à la fois des déclarations et des définitions. Il est uniquement recommandé de placer des déclarations dans des fichiers d'en-tête et des définitions dans des fichiers .c ou .cpp.

D'autre part, il est possible d'avoir un modèle de compilation dans lequel le compilateur sait très bien s'il importe d'importer la déclaration d'un symbole global qui est défini dans un autre module , ou s'il s'agit de la compilation d'un symbole global fourni par le module actuel . Ce n'est que dans ce dernier cas que le compilateur doit mettre ce symbole (par exemple une variable) dans le fichier objet actuel.

Par exemple, dans GNU Pascal vous pouvez écrire une unité a dans un fichier a.pas comme ça:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

où la variable globale est déclarée et initialisée dans le même fichier source.

Ensuite, vous pouvez avoir différentes unités qui importent un et utilisent la variable globale MyStaticVariable, par exemple une unité b (b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

et une unité c (c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

Enfin, vous pouvez utiliser les unités b et c dans un programme principal m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Vous pouvez compiler ces fichiers séparément:

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

puis produire un exécutable avec:

$ gpc -o m m.o a.o b.o c.o

et lancez-le:

$ ./m
1
2
3

L'astuce ici est que lorsque le compilateur voit une directive utilise dans un module de programme (par exemple utilise a dans b.pas), il n'inclut pas le correspondant Fichier .pas, mais recherche un fichier .gpi, c'est-à-dire un fichier d'interface précompilé (voir la documentation ). Celles-ci .gpi les fichiers sont générés par le compilateur avec le .o fichiers lorsque chaque module est compilé. Ainsi, le symbole global MyStaticVariable n'est défini qu'une seule fois dans le fichier objet a.o.

Java fonctionne de la même manière: lorsque le compilateur importe ensuite une classe A dans la classe B, il regarde le fichier de classe pour A et n'a pas besoin du fichier A.Java. Ainsi, toutes les définitions et initialisations pour la classe A peuvent être placées dans un seul fichier source.

Pour en revenir à C++, la raison pour laquelle en C++ vous devez définir des membres de données statiques dans un fichier séparé est plus liée au modèle de compilation C++ qu'aux limitations imposées par l'éditeur de liens ou d'autres outils utilisés par le compilateur. En C++, importer certains symboles signifie construire leur déclaration dans le cadre de l'unité de compilation actuelle. Ceci est très important, entre autres, en raison de la manière dont les modèles sont compilés. Mais cela implique que vous ne pouvez/ne devez pas définir de symboles globaux (fonctions, variables, méthodes, membres de données statiques) dans un fichier inclus, sinon ces symboles pourraient être définis de manière multipliée dans les fichiers objets compilés.

16
Giorgio

Étant donné que les membres statiques sont partagés entre TOUTES les instances d'une classe, ils doivent être définis en un et un seul endroit. Vraiment, ce sont des variables globales avec certaines restrictions d'accès.

Si vous essayez de les définir dans l'en-tête, ils seront définis dans chaque module qui inclut cet en-tête, et vous obtiendrez des erreurs lors de la liaison car il trouvera toutes les définitions en double.

Oui, c'est au moins en partie un problème historique datant de cfront; un compilateur pourrait être écrit qui créerait une sorte de "static_members_of_everything.cpp" caché et un lien vers cela. Cependant, cela romprait la compatibilité descendante, et cela ne présenterait aucun avantage réel.

43
mjfgates

Il y a une grande différence entre C++ et Java.

Java fonctionne sur sa propre machine virtuelle qui crée tout dans son propre environnement d'exécution. Si une définition apparaît plusieurs fois, agira simplement sur le même objet que l'environnement d'exécution connaît ultimement.

En C++, il n'y a pas de "propriétaire ultime des connaissances": C++, C, Fortran Pascal, etc. sont tous "traducteurs" d'un code source (fichier CPP) dans un format intermédiaire (le fichier OBJ ou ".o", selon l'OS) où les instructions sont traduites en instructions machine et les noms deviennent des adresses indirectes médiées par une table de symboles.

Un programme n'est pas créé par le compilateur, mais par un autre programme (le "linker"), qui relie tous les OBJ (quel que soit le langage dont ils proviennent) en redirigeant toutes les adresses qui sont vers des symboles, vers leur définition efficace.

Par la façon dont l'éditeur de liens fonctionne, une définition (ce qui crée l'espace physique pour une variable) doit être unique.

Notez que C++ ne lie pas par lui-même, et que l'éditeur de liens n'est pas émis par les spécifications C++: l'éditeur de liens existe en raison de la façon dont les modules du système d'exploitation sont construits (généralement en C et ASM). C++ doit l'utiliser tel qu'il est.

Maintenant: un fichier d'en-tête est quelque chose à "coller" dans plusieurs fichiers CPP. Chaque fichier CPP est traduit indépendamment de tous les autres. Un compilateur traduisant différents fichiers CPP, tous recevant une définition identique placera le "code de création" pour l'objet défini dans tous les objets résultants.

Le compilateur ne sait pas (et ne saura jamais) si tous ces objets seront jamais utilisés ensemble pour former un seul programme ou séparément pour former différents programmes indépendants.

L'éditeur de liens ne sait pas comment et pourquoi les définitions existent et d'où elles viennent (il ne connaît même pas C++: chaque "langage statique" peut produire des définitions et des références à lier). Il sait simplement qu'il existe des références à un "symbole" donné qui est "défini" à une adresse résultante donnée.

S'il existe plusieurs définitions (ne confondez pas les définitions et les références) pour un symbole donné, l'éditeur de liens n'a aucune connaissance (étant indépendant du langage) de ce qu'il faut en faire.

C'est comme fusionner un certain nombre de villes pour former une grande ville: si vous avez deux "Time square" et un certain nombre de personnes venant de l'extérieur qui demandent à aller à "Time square ", vous ne pouvez pas décider sur une base purement technique (sans aucune connaissance de la politique qui a attribué ces noms et sera en charge de les gérer) dans quel endroit exact les envoyer.

6
Emilio Garavaglia

La raison probable de ceci est que cela maintient le langage C++ implémentable dans des environnements où le fichier objet et le modèle de liaison ne prennent pas en charge la fusion de plusieurs définitions à partir de plusieurs fichiers objet.

Une déclaration de classe (appelée déclaration pour de bonnes raisons) est tirée dans plusieurs unités de traduction. Si la déclaration contenait des définitions de variables statiques, vous vous retrouveriez avec plusieurs définitions dans plusieurs unités de traduction (Et rappelez-vous, ces noms ont un lien externe.)

Cette situation est possible, mais nécessite que l'éditeur de liens gère plusieurs définitions sans se plaindre.

(Et notez que cela entre en conflit avec la règle de définition unique, sauf si cela peut être fait en fonction du type de symbole ou du type de section dans lequel il est placé.)

6
Kaz

Son requis car sinon le compilateur ne sait pas où mettre la variable. Chaque fichier cpp est compilé individuellement et ne connaît pas l'autre. L'éditeur de liens résout les variables, les fonctions, etc. Personnellement, je ne vois pas la différence entre les membres vtable et statiques (nous n'avons pas à choisir dans quel fichier la vtable est définie).

Je suppose principalement qu'il est plus facile pour les rédacteurs de compilateurs de l'implémenter de cette façon. Il existe des variables statiques en dehors de la classe/structure et peut-être soit pour des raisons de cohérence, soit parce qu'il serait "plus facile à mettre en œuvre" pour les rédacteurs du compilateur, ils ont défini cette restriction dans les normes.

5
user2528

Je pense que j'ai trouvé la raison. La définition de la variable static dans un espace séparé permet de l'initialiser à n'importe quelle valeur. S'il n'est pas initialisé, il sera par défaut à 0.

Avant C++ 11, l'initialisation en classe n'était pas autorisée en C++. Donc on ne peut pas écrire comme:

struct X
{
  static int i = 4;
};

Ainsi maintenant pour initialiser la variable il faut l'écrire en dehors de la classe comme:

struct X
{
  static int i;
};
int X::i = 4;

Comme indiqué dans d'autres réponses également, int X::i est maintenant un global et le fait de déclarer global dans de nombreux fichiers provoque une erreur de liaison de symboles multiples.

Il faut donc déclarer une variable classe static à l'intérieur d'une unité de traduction distincte. Cependant, on peut toujours soutenir que la méthode suivante devrait demander au compilateur de ne pas créer plusieurs symboles

static int X::i = 4;
^^^^^^
2
iammilind

A :: x est juste une variable globale mais un espace de noms à A, et avec des restrictions d'accès.

Quelqu'un doit encore le déclarer, comme toute autre variable globale, et cela peut même être fait dans un projet qui est lié statiquement au projet contenant le reste du code A.

J'appellerais tout cela une mauvaise conception, mais il y a quelques fonctionnalités que vous pouvez exploiter de cette façon:

  1. l'ordre d'appel du constructeur ... Peu important pour un int, mais pour un membre plus complexe qui accède peut-être à d'autres variables statiques ou globales, il peut être critique.

  2. l'initialiseur statique - vous pouvez laisser un client décider à quoi A :: x doit être initialisé.

  3. en c ++ et c, parce que vous avez un accès complet à la mémoire via des pointeurs, l'emplacement physique des variables est important. Il y a des choses très coquines que vous pouvez exploiter en fonction de l'emplacement d'une variable dans un objet lien.

Je doute que ce soit "pourquoi" cette situation est apparue. C'est probablement juste une évolution de C se transformant en C++, et un problème de compatibilité descendante qui vous empêche de changer le langage maintenant.

0
James Podesta