Perché i membri di dati statici devono essere definiti al di fuori della classe separatamente in C ++ (a differenza di Java)?


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

Non vedo la necessità di averlo A::xdefinito separatamente in un file .cpp (o nello stesso file per i modelli). Perché non possono essere A::xdichiarati e definiti allo stesso tempo?

È stato proibito per motivi storici?

La mia domanda principale è: interesserà qualsiasi funzionalità se statici membri dei dati fossero dichiarati / definiti contemporaneamente (lo stesso di Java )?


Come best practice, è generalmente consigliabile includere la variabile statica in un metodo statico (possibilmente come statico locale) per evitare problemi di ordine di inizializzazione.
Tamás Szelei,

2
Questa regola è in realtà un po 'rilassata in C ++ 11. i membri const const di solito non devono più essere definiti. Vedi: en.wikipedia.org/wiki/…
mirk

4
@afishwhoswimsaround: specificare regole generalizzate per tutte le situazioni non è una buona idea (le migliori pratiche dovrebbero essere applicate con il contesto). Qui stai cercando di risolvere un problema che non esiste. Il problema dell'ordine di inizializzazione riguarda solo gli oggetti che hanno costruttori e accedono ad altri oggetti di durata della memoria statica. Poiché "x" è int, il primo non si applica poiché "x" è privato, il secondo non si applica. In terzo luogo, questo non ha nulla a che fare con la domanda.
Martin York,

1
Appartiene allo Stack Overflow?
Lightness Races con Monica

2
C ++ 17 consente inizializzazione linea di membri dati statici (anche per i tipi non interi): inline static int x[] = {1, 2, 3};. Vedi en.cppreference.com/w/cpp/language/static#Static_data_members
Vladimir Reshetnikov

Risposte:


15

Penso che la limitazione che hai preso in considerazione non sia correlata alla semantica (perché qualcosa dovrebbe cambiare se l'inizializzazione fosse definita nello stesso file?) Ma piuttosto al modello di compilazione C ++ che, per motivi di compatibilità con le versioni precedenti, non può essere facilmente modificato perché o diventare troppo complesso (supportando un nuovo modello di compilazione e quello esistente contemporaneamente) o non consentirebbe di compilare il codice esistente (introducendo un nuovo modello di compilazione e rilasciando quello esistente).

Il modello di compilazione C ++ deriva da quello di C, in cui si importano le dichiarazioni in un file di origine includendo i file (di intestazione). In questo modo, il compilatore vede esattamente un grande file sorgente, contenente tutti i file inclusi e tutti i file inclusi da quei file, in modo ricorsivo. Questo ha un grande vantaggio dell'IMO, ovvero che semplifica l'implementazione del compilatore. Ovviamente, puoi scrivere qualsiasi cosa nei file inclusi, cioè sia dichiarazioni che definizioni. È buona norma inserire le dichiarazioni nei file di intestazione e le definizioni nei file .c o .cpp.

D'altra parte, è possibile avere un modello di compilazione in cui il compilatore sappia molto bene se sta importando la dichiarazione di un simbolo globale che è definita in un altro modulo o se sta compilando la definizione di un simbolo globale fornita da il modulo corrente . Solo in quest'ultimo caso il compilatore deve inserire questo simbolo (ad es. Una variabile) nel file oggetto corrente.

Ad esempio, in GNU Pascal puoi scrivere un'unità ain un file a.pascome questo:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

dove la variabile globale viene dichiarata e inizializzata nello stesso file di origine.

Quindi puoi avere diverse unità che importano a e usano la variabile globale MyStaticVariable, ad esempio un'unità b ( b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

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

e un'unità c ( c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

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

Finalmente puoi usare le unità bec in un programma principale m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

È possibile compilare questi file separatamente:

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

e quindi produrre un eseguibile con:

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

ed eseguilo:

$ ./m
1
2
3

Il trucco qui è che quando il compilatore vede una direttiva usi in un modulo di programma (ad esempio usa a in b.pas), non include il file .pas corrispondente, ma cerca un file .gpi, ovvero un pre-compilato file di interfaccia (consultare la documentazione ). Questi .gpifile vengono generati dal compilatore insieme ai .ofile quando viene compilato ciascun modulo. Quindi il simbolo globale MyStaticVariableviene definito una sola volta nel file oggetto a.o.

Java funziona in modo simile: quando quindi il compilatore importa una classe A in classe B, cerca il file di classe per A e non ha bisogno del file A.java. Quindi tutte le definizioni e le inizializzazioni per la classe A possono essere inserite in un file sorgente.

Tornando a C ++, il motivo per cui in C ++ è necessario definire membri di dati statici in un file separato è più correlato al modello di compilazione C ++ che alle limitazioni imposte dal linker o da altri strumenti utilizzati dal compilatore. In C ++, importare alcuni simboli significa costruire la loro dichiarazione come parte dell'attuale unità di compilazione. Questo è molto importante, tra le altre cose, a causa del modo in cui i modelli vengono compilati. Ciò implica che non è possibile / non si devono definire simboli globali (funzioni, variabili, metodi, membri di dati statici) in un file incluso, altrimenti questi simboli potrebbero essere definiti in modo multiplo nei file oggetto compilati.


42

Poiché i membri statici sono condivisi tra TUTTE le istanze di una classe, devono essere definiti in un'unica posizione. In realtà, sono variabili globali con alcune restrizioni di accesso.

Se provi a definirli nell'intestazione, saranno definiti in ogni modulo che include quell'intestazione e otterrai errori durante il collegamento poiché trova tutte le definizioni duplicate.

Sì, questo è almeno in parte un problema storico risalente al lungomare; si potrebbe scrivere un compilatore che crei una sorta di "static_members_of_everything.cpp" nascosto e si colleghi a quello. Tuttavia, romperebbe la compatibilità all'indietro e non ci sarebbe alcun reale beneficio nel farlo.


2
La mia domanda non è la ragione del comportamento attuale, ma piuttosto la giustificazione per tale grammatica linguistica. In altre parole, supponiamo che se le staticvariabili sono dichiarate / definite nello stesso posto (come Java), cosa può andare storto?
iammilind,

8
@iammilind Penso che tu non capisca che la grammatica è necessaria a causa della spiegazione di questa risposta. Ora perché? A causa del modello di compilazione di C (e C ++): i file c e cpp sono i file di codice reale che vengono compilati separatamente come programmi separati, quindi vengono collegati insieme per creare un eseguibile completo. Le intestazioni non sono in realtà codice per il compilatore, sono solo testo da copiare e incollare all'interno dei file c e cpp. Ora, se qualcosa viene definito più volte, non può essere compilato, allo stesso modo non verrà compilato se si hanno più variabili locali con lo stesso nome.
Klaim,

1
@Klaim, che dire dei staticmembri in template? Sono consentiti in tutti i file di intestazione in quanto devono essere visibili. Non sto contestando questa risposta, ma non corrisponde anche alla mia domanda.
iammilind,

I modelli @iammilind non sono codici reali, sono codici che generano codice. Ogni istanza di un modello ha una e una sola istanza statica di ciascuna dichiarazione statica fornita dal compilatore. Devi ancora definire l'istanza ma quando definisci un modello di un'istanza, non è un codice reale, come detto sopra. I modelli sono, letteralmente, modelli di codice per il compilatore per generare codice.
Klaim,

2
@iammilind: i modelli sono generalmente istanziati in ogni file oggetto, comprese le loro variabili statiche. Su Linux con file oggetto ELF, il compilatore contrassegna le istanze come simboli deboli , il che significa che il linker combina più copie della stessa istanza. La stessa tecnologia potrebbe essere utilizzata per consentire la definizione di variabili statiche nei file di intestazione, quindi la ragione per cui non viene eseguita è probabilmente una combinazione di ragioni storiche e considerazioni sulle prestazioni della compilazione. Si spera che l'intero modello di compilazione sarà corretto una volta che il prossimo standard C ++ includerà i moduli .
han

6

Il probabile motivo di ciò è che ciò mantiene il linguaggio C ++ implementabile in ambienti in cui il file oggetto e il modello di collegamento non supportano l'unione di più definizioni da più file oggetto.

Una dichiarazione di classe (chiamata una dichiarazione per buoni motivi) viene inserita in più unità di traduzione. Se la dichiarazione conteneva definizioni per variabili statiche, si otterrebbero più definizioni in più unità di traduzione (e ricorda, questi nomi hanno un collegamento esterno).

Tale situazione è possibile, ma richiede al linker di gestire più definizioni senza lamentarsi.

(E nota che questo è in conflitto con la One Definition Rule, a meno che non possa essere fatto in base al tipo di simbolo o al tipo di sezione in cui è inserito).


6

C'è una grande differenza tra C ++ e Java.

Java opera sulla propria macchina virtuale che crea tutto nel proprio ambiente di runtime. Se una definizione sembra essere vista più di una volta, agirà semplicemente sullo stesso oggetto che l'ambiente di runtime conosce alla fine.

In C ++ non esiste un "proprietario della conoscenza finale": C ++, C, Fortran Pascal ecc. Sono tutti "traduttori" da un codice sorgente (file CPP) in un formato intermedio (il file OBJ o il file ".o", a seconda del il sistema operativo) in cui le istruzioni vengono tradotte in istruzioni macchina e i nomi diventano indirizzi indiretti mediati da una tabella dei simboli.

Un programma non è creato dal compilatore, ma da un altro programma (il "linker"), che unisce tutti gli OBJ (indipendentemente dalla lingua da cui provengono) reindirizzando tutti gli indirizzi che sono verso simboli, verso il loro definizione efficace.

A proposito del linker, una definizione (ciò che crea lo spazio fisico per una variabile) deve essere unica.

Si noti che C ++ non si collega da solo e che il linker non viene emesso dalle specifiche C ++: il linker esiste a causa del modo in cui i moduli del sistema operativo sono costruiti (di solito in C e ASM). Il C ++ deve usarlo così com'è.

Ora: un file di intestazione è qualcosa da "incollare" in diversi file CPP. Ogni file CPP viene tradotto indipendentemente da ogni altro. Un compilatore che traduce file CPP diversi, tutti ricevendo in una stessa definizione inserirà il " codice di creazione " per l'oggetto definito in tutti gli OBJ risultanti.

Il compilatore non sa (e non saprà mai) se tutti quegli OBJ saranno mai usati insieme per formare un singolo programma o separatamente per formare diversi programmi indipendenti.

Il linker non sa come e perché esistono le definizioni e da dove provengono (non sa nemmeno del C ++: ogni "linguaggio statico" può produrre definizioni e riferimenti da collegare). Sa solo che ci sono riferimenti a un dato "simbolo" che è "definito" in un dato indirizzo risultante.

Se ci sono più definizioni (non confondere le definizioni con i riferimenti) per un dato simbolo, il linker non ha alcuna conoscenza (essendo il linguaggio agnostico) su cosa farne.

È come unire un numero di città per formare una grande città: se ti accorgi che hanno due " Time square " e un certo numero di persone che provengono da fuori chiedendo di andare a " Time square ", non puoi decidere su una base tecnica pura (senza alcuna conoscenza della politica che ha assegnato quei nomi e sarà incaricato di gestirli) nel posto esatto in cui inviarli.


3
La differenza tra Java e C ++ rispetto ai simboli globali non è connessa con Java che ha una macchina virtuale, ma piuttosto con il modello di compilazione C ++. A questo proposito, non metterei Pascal e C ++ nella stessa categoria. Piuttosto raggrupperei C e C ++ come "linguaggi in cui le dichiarazioni importate sono incluse e compilate insieme al file sorgente principale" al contrario di Java e Pascal (e forse OCaml, Scala, Ada, ecc.) Come "linguaggi in cui il le dichiarazioni importate vengono cercate dal compilatore in file precompilati contenenti informazioni sui simboli esportati ".
Giorgio,

1
@Giorgio: il riferimento a Java potrebbe non essere il benvenuto, ma penso che la risposta di Emilio sia per lo più corretta arrivando all'essenza del problema, vale a dire la fase del file oggetto / linker dopo la compilazione separata.
ixache,

5

È richiesto perché altrimenti il ​​compilatore non sa dove inserire la variabile. Ogni file cpp è compilato individualmente e non conosce l'altro. Il linker risolve variabili, funzioni, ecc. Personalmente non vedo quale sia la differenza tra i membri vtable e statici (non dobbiamo scegliere in quale file sono definiti i file vtable).

Presumo principalmente che sia più facile per gli autori di compilatori implementarlo in quel modo. Esistono varianti statiche al di fuori della classe / struttura e forse per ragioni di coerenza o perché sarebbe "più facile da implementare" per gli autori di compilatori che hanno definito tale restrizione negli standard.


2

Penso di aver trovato il motivo. La definizione di una staticvariabile in uno spazio separato consente di inizializzarla su qualsiasi valore. Se non inizializzato, verrà impostato automaticamente su 0.

Prima di C ++ 11 l'inizializzazione in classe non era consentita in C ++. Quindi non si può scrivere come:

struct X
{
  static int i = 4;
};

Quindi ora per inizializzare la variabile bisogna scriverla al di fuori della classe come:

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

Come discusso anche in altre risposte, int X::iora è globale e dichiarare globale in molti file provoca errori di collegamento a più simboli.

Quindi si deve dichiarare una staticvariabile di classe all'interno di un'unità di traduzione separata. Tuttavia, si può ancora sostenere che il modo seguente dovrebbe istruire il compilatore a non creare più simboli

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

0

A :: x è solo una variabile globale ma lo spazio dei nomi è A e con restrizioni di accesso.

Qualcuno deve ancora dichiararlo, come qualsiasi altra variabile globale, e ciò può anche essere fatto in un progetto che è staticamente collegato al progetto contenente il resto del codice A.

Definirei tutti questi cattivi design, ma ci sono alcune funzionalità che puoi sfruttare in questo modo:

  1. l'ordine di chiamata del costruttore ... Non importante per un int, ma per un membro più complesso che può accedere ad altre variabili statiche o globali, può essere critico.

  2. l'inizializzatore statico: puoi consentire a un client di decidere in quale A: x deve essere inizializzato.

  3. in c ++ ec, poiché si ha pieno accesso alla memoria tramite puntatori, la posizione fisica delle variabili è significativa. Ci sono cose molto cattive che puoi sfruttare in base a dove si trova una variabile in un oggetto link.

Dubito che questi siano "perché" questa situazione è sorta. Probabilmente è solo un'evoluzione di C che si trasforma in C ++ e un problema di compatibilità con le versioni precedenti che ti impedisce di cambiare la lingua ora.


2
questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nelle precedenti 6 risposte
moscerino del
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.