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

41
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::xdéfini séparément dans un fichier .cpp (ou le même fichier pour les modèles). Pourquoi ne peut pas être A::xdéclaré et défini en même temps?

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

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

iammilind
la source
Il est généralement préférable d'envelopper votre variable statique dans une méthode statique (éventuellement en tant que variable locale locale) pour éviter les problèmes d'ordre d'initialisation.
Tamás Szelei
2
Cette règle est un peu assouplie dans C ++ 11. Les membres statiques const ne doivent généralement plus être définis. Voir: fr.wikipedia.org/wiki/…
mirk
4
@afishwhoswimsaround: Spécifier des règles trop généralisées pour toutes les situations n'est pas une bonne idée (les meilleures pratiques doivent être appliquées avec le contexte). Ici, vous essayez de résoudre un problème qui n'existe pas. Le problème d'ordre d'initialisation concerne uniquement les objets comportant des constructeurs et accédant à d'autres objets de durée de stockage statique. Puisque 'x' est int, le premier ne s'applique pas, car 'x' est privé, le second ne s'applique pas. Troisièmement, cela n'a rien à voir avec la question.
Martin York
1
Appartient au débordement de pile?
Courses de légèreté avec Monica
2
C ++ 17 permet l' initialisation ligne d'éléments de données statiques (même pour des types non entiers) inline static int x[] = {1, 2, 3};. Voir fr.cppreference.com/w/cpp/language/static#Static_data_members
Vladimir Reshetnikov

Réponses:

15

Je pense que la limitation que vous avez envisagée n’est pas liée à la sémantique (pourquoi changer quelque chose si l’initialisation est définie dans le même fichier?), Mais plutôt au modèle de compilation C ++ qui, pour des raisons de compatibilité ascendante, ne peut pas être changé facilement soit devenir trop complexe (prendre en charge simultanément un nouveau modèle de compilation et le modèle existant), soit 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 ++ provient de celui de C, dans lequel vous importez des déclarations dans un fichier source en incluant des fichiers (en-tête). De cette manière, le compilateur voit exactement un gros fichier source, contenant tous les fichiers inclus et tous les fichiers inclus à partir de ces fichiers, de manière récursive. Cela présente un gros avantage pour OMI, à savoir que cela 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 recommandé de placer les déclarations dans les fichiers d’en-tête et les définitions dans les 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 la déclaration d'un symbole global défini dans un autre module ou s'il compile la définition 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é adans un fichier a.pascomme celui-ci:

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 a 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

et ensuite 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 uses dans un module de programme (par exemple, utilise a dans b.pas), il n'inclut pas le fichier .pas correspondant, mais recherche un fichier .gpi, c'est-à-dire un fichier pré-compilé. fichier d'interface (voir la documentation ). Ces .gpifichiers sont générés par le compilateur avec les .ofichiers lors de la compilation de chaque module. Le symbole global MyStaticVariablen’est donc défini qu’une fois dans le fichier objet a.o.

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

Pour revenir à C ++, la raison pour laquelle vous devez définir des membres de données statiques dans un fichier séparé est davantage 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 des symboles signifie construire leur déclaration dans l'unité de compilation en cours. Ceci est très important, entre autres choses, à cause de la manière dont les modèles sont compilés. Mais cela implique que vous ne pouvez / ne devez définir aucun symbole global (fonctions, variables, méthodes, membres de données statiques) dans un fichier inclus, sinon ces symboles pourraient être définis de manière multiple dans les fichiers d'objet compilés.

Giorgio
la source
42

Les membres statiques étant partagés entre TOUTES les instances d'une classe, ils doivent être définis dans un seul et même emplacement. En réalité, 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 lors de la recherche de toutes les définitions en double.

Oui, il s’agit au moins en partie d’une question historique qui remonte à l’avant; un compilateur pourrait être écrit pour créer une sorte de "static_members_of_everything.cpp" caché et un lien vers cela. Cependant, cela éliminerait la compatibilité en amont et ne présenterait aucun avantage réel.

mjfgates
la source
2
Ma question n'est pas la raison du comportement actuel, mais plutôt la justification de cette grammaire linguistique. En d'autres termes, supposons que si les staticvariables sont déclarées / définies au même endroit (comme Java), qu'est-ce qui peut mal se passer?
iammilind
8
@iammilind Je pense que vous ne comprenez pas que la grammaire est nécessaire à cause de l'explication de cette réponse. Maintenant pourquoi? En raison du modèle de compilation de C (et C ++): les fichiers c et cpp sont les véritables fichiers de code qui sont compilés séparément, comme des programmes distincts, puis sont liés pour constituer un exécutable complet. Les en-têtes ne sont pas vraiment du code pour le compilateur, ils ne sont que du texte à copier et coller dans les fichiers cpp et cpp. Maintenant, si quelque chose est défini plusieurs fois, il ne peut pas le compiler, de la même manière qu'il ne le sera pas si vous avez plusieurs variables locales portant le même nom.
Klaim
1
@ Klaim, qu'en est-il des staticmembres template? Ils sont autorisés dans tous les fichiers d'en-tête car ils doivent être visibles. Je ne conteste pas cette réponse, mais elle ne correspond pas non plus à ma question.
iammilind
Les modèles @iammilind ne sont pas du code réel, ce sont des codes qui génèrent du code. Chaque instance d'un modèle possède une et une seule instance statique de chaque déclaration statique fournie par le compilateur. Vous devez toujours définir l'instance, mais comme vous définissez un modèle d'instance, il ne s'agit pas d'un code réel, comme indiqué ci-dessus. Les modèles sont littéralement des modèles de code permettant au compilateur de générer du code.
Klaim
2
@iammilind: les modèles sont généralement instanciés dans chaque fichier objet, y compris leurs variables statiques. Sous Linux avec des fichiers objet ELF, le compilateur marque les instanciations comme des symboles faibles , ce qui signifie que l'éditeur de liens associe plusieurs copies d'une même instanciation. La même technologie pourrait être utilisée pour permettre de définir des variables statiques dans les fichiers d’en-tête. La raison pour laquelle cela n’a pas été fait est probablement une combinaison de raisons historiques et de considérations relatives aux performances de la compilation. Nous espérons que le modèle de compilation sera entièrement corrigé une fois que la prochaine norme C ++ aura incorporé des modules .
han
6

La raison probable en est que le langage C ++ peut être implémenté 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 extraite dans plusieurs unités de traduction. Si la déclaration contenait des définitions pour les variables statiques, vous vous retrouveriez avec plusieurs définitions dans plusieurs unités de traduction (rappelez-vous, ces noms ont un lien externe.)

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

(Notez que ceci est en conflit avec la règle de définition unique, à moins que cela ne puisse être fait en fonction du type de symbole ou du type de section dans lequel il est placé.)

Kaz
la source
6

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 plus d'une fois, agira simplement sur le même objet que l'environnement d'exécution connaît ultimatement.

En C ++, il n’existe pas de "propriétaire ultime de connaissances": C ++, C, Fortran Pascal, etc. sont tous "traducteurs" d’un code source (fichier CPP) dans un format intermédiaire (le fichier OBJ ou le fichier ".o", 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 "lieur"), qui relie tous les OBJ-s ensemble (quelle que soit leur langue) en redirigeant toutes les adresses dirigées vers des symboles, vers leur définition efficace.

De par le mode de fonctionnement de l'éditeur de liens, une définition (ce qui crée l'espace physique d'une variable) doit être unique.

Notez que C ++ ne lie pas par lui-même et que l'éditeur de liens n'est pas publié par les spécifications C ++: il existe en raison de la structure des modules du système d'exploitation (généralement en C et ASM). C ++ doit l'utiliser tel quel.

Maintenant: un fichier d'en-tête est quelque chose qui doit être "collé" dans plusieurs fichiers CPP. Chaque fichier CPP est traduit indépendamment des autres. Un compilateur traduisant différents fichiers CPP, recevant tous dans une même définition, placera le " code de création " de l'objet défini dans tous les OBJ résultants.

Le compilateur ne sait pas (et ne saura jamais) si tous ces OBJ 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 sait même pas à propos de C ++: chaque "langage statique" peut produire des définitions et des références à lier). Il sait simplement qu'il y a 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 avec les références) pour un symbole donné, l'éditeur de liens n'a aucune connaissance (ne tenant pas compte de la langue) de ce qu'il faut en faire.

C'est comme si vous fusionniez plusieurs villes pour former une grande ville: si vous vous retrouvez avec deux " Time square " et si plusieurs personnes venant de l'extérieur demandent d'aller à " Time square ", vous ne pouvez pas choisir uniquement sur des bases techniques. (sans aucune connaissance de la politique qui a assigné ces noms et sera chargé de les gérer) dans quel endroit exact les envoyer.

Emilio Garavaglia
la source
3
La différence entre Java et C ++ en ce qui concerne les symboles globaux n’est pas liée au fait que Java dispose d’une machine virtuelle, mais bien au modèle de compilation C ++. À cet égard, je ne mettrais pas Pascal et C ++ dans la même catégorie. Je regrouperais plutôt C et C ++ ensemble en tant que "langages dans lesquels les déclarations importées sont incluses et compilées avec le fichier source principal", par opposition à Java et Pascal (et peut-être à OCaml, Scala, Ada, etc.) en tant que "langages dans lesquels Les déclarations importées sont recherchées par le compilateur dans des fichiers précompilés contenant des informations sur les symboles exportés ".
Giorgio
1
@Giorgio: la référence à Java n'est peut-être pas bienvenue, mais je pense que la réponse d'Emilio est généralement exacte en décrivant l'essentiel du problème, à savoir la phase de fichier objet / éditeur de liens après une compilation séparée.
ixache
5

C'est obligatoire car sinon le compilateur ne sait pas où placer 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 quelle est la différence entre les membres vtable et static (nous n'avons pas à choisir le fichier dans lequel la vtable est définie).

J'assume surtout qu'il est plus facile pour les auteurs de compilateur de le mettre en œuvre de cette façon. Les variables statiques en dehors de la classe / structure existent et peut-être soit pour des raisons de cohérence, soit parce que ce serait "plus facile à mettre en oeuvre" pour les rédacteurs de compilateur, ils ont défini cette restriction dans les normes.


la source
2

Je pense avoir trouvé la raison. Définir une staticvariable dans un espace séparé permet de l'initialiser à n'importe quelle valeur. S'il n'est pas initialisé, la valeur par défaut est 0.

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

struct X
{
  static int i = 4;
};

Donc maintenant, pour initialiser la variable, il faut l’écrire en dehors de la classe:

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

Comme indiqué dans d'autres réponses également, int X::iest maintenant un global et la déclaration de globale dans de nombreux fichiers provoque plusieurs erreurs de liaison de symboles.

Ainsi, il faut déclarer une staticvariable de classe dans une unité de traduction distincte. Cependant, on peut toujours affirmer que la méthode suivante doit indiquer au compilateur de ne pas créer plusieurs symboles

static int X::i = 4;
^^^^^^
iammilind
la source
0

A :: x est juste une variable globale mais l'espace de nom est attribué à 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 lié statiquement au projet contenant le reste du code A.

J'appellerais cela du mauvais design, mais il y a quelques fonctionnalités que vous pouvez exploiter de cette façon:

  1. l'ordre d'appel du constructeur ... Pas important pour un int, mais pour un membre plus complexe pouvant accéder à d'autres variables statiques ou globales, cela peut être critique.

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

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

Je doute que ce soit "pourquoi" cette situation s'est produite. Il s’agit probablement d’une évolution de C en C ++ et d’un problème de compatibilité ascendante qui vous empêche de changer de langage maintenant.

James Podesta
la source
2
cela ne semble rien offrir de substantiel sur les points évoqués et expliqués dans les 6 réponses précédentes
gnat