Dlaczego elementy danych statycznych muszą być definiowane poza klasą osobno w C ++ (w przeciwieństwie do Java)?

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

Nie widzę potrzeby A::xosobnego definiowania w pliku .cpp (lub w tym samym pliku dla szablonów). Dlaczego nie można A::xzadeklarować i zdefiniować jednocześnie?

Czy zostało to zabronione z powodów historycznych?

Moje główne pytanie brzmi: czy wpłynie to na jakąkolwiek funkcjonalność, jeśli staticelementy danych zostały zadeklarowane / zdefiniowane w tym samym czasie (tak samo jak Java )?

iammilind
źródło
Najlepszym rozwiązaniem jest na ogół zawinięcie zmiennej statycznej w metodę statyczną (być może jako lokalną statyczną), aby uniknąć problemów z kolejnością inicjalizacji.
Tamás Szelei,
2
Ta zasada jest nieco złagodzona w C ++ 11. stałe elementy statyczne zwykle nie muszą być już definiowane. Patrz: en.wikipedia.org/wiki/…
mirk
4
@afishwhoswimsaround: Określanie ogólnych reguł dla wszystkich sytuacji nie jest dobrym pomysłem (najlepsze praktyki powinny być stosowane w kontekście). Tutaj próbujesz rozwiązać problem, który nie istnieje. Problem z kolejnością inicjowania dotyczy tylko obiektów, które mają konstruktory i uzyskują dostęp do innych obiektów czasu przechowywania statycznego. Ponieważ „x” jest int, pierwsze nie ma zastosowania, ponieważ „x” jest prywatne, drugie nie ma zastosowania. Po trzecie, nie ma to nic wspólnego z pytaniem.
Martin York,
1
Należy do przepełnienia stosu?
Lekkość ściga się z Moniką
2
C ++ 17 pozwala na rolkach inicjalizacji statycznych danych (nawet dla typu niecałkowitą) inline static int x[] = {1, 2, 3};. Zobacz en.cppreference.com/w/cpp/language/static#Static_data_members
Vladimir Reshetnikov

Odpowiedzi:

15

Myślę, że rozważane ograniczenie nie jest związane z semantyką (dlaczego coś miałoby się zmieniać, gdyby inicjalizacja została zdefiniowana w tym samym pliku?), Ale raczej z modelem kompilacji C ++, którego ze względu na kompatybilność wsteczną nie można łatwo zmienić, ponieważ albo stają się zbyt skomplikowane (obsługując jednocześnie nowy model kompilacji i istniejący) lub nie pozwalają na kompilację istniejącego kodu (poprzez wprowadzenie nowego modelu kompilacji i usunięcie istniejącego).

Model kompilacji C ++ wywodzi się z modelu C, w którym importujesz deklaracje do pliku źródłowego, włączając pliki (nagłówkowe). W ten sposób kompilator widzi dokładnie jeden duży plik źródłowy, zawierający wszystkie zawarte pliki i wszystkie pliki z tych plików, rekurencyjnie. Ma to jedną wielką zaletę IMO, a mianowicie to, że ułatwia wdrożenie kompilatora. Oczywiście w zapisanych plikach można pisać wszystko, tzn. Deklaracje i definicje. Dobrą praktyką jest umieszczanie deklaracji w plikach nagłówkowych i definicji w plikach .c lub .cpp.

Z drugiej strony możliwe jest posiadanie modelu kompilacji, w którym kompilator doskonale wie, czy importuje deklarację symbolu globalnego zdefiniowanego w innym module , czy też kompiluje definicję symbolu globalnego dostarczoną przez obecny moduł . Tylko w tym drugim przypadku kompilator musi umieścić ten symbol (np. Zmienną) w bieżącym pliku obiektowym.

Na przykład w GNU Pascal możesz zapisać jednostkę aw pliku a.pastakim jak ten:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

gdzie zmienna globalna jest zadeklarowana i zainicjowana w tym samym pliku źródłowym.

Następnie możesz mieć różne jednostki, które importują a i używają zmiennej globalnej MyStaticVariable, np. Jednostkę b ( b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

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

oraz jednostka c ( c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

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

Wreszcie możesz użyć jednostek b i c w głównym programie m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Możesz skompilować te pliki osobno:

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

a następnie utworzyć plik wykonywalny z:

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

i uruchom to:

$ ./m
1
2
3

Sztuczka polega na tym, że gdy kompilator widzi dyrektywę use w module programu (np. Używa a w b.pas), nie zawiera odpowiedniego pliku .pas, ale szuka pliku .gpi, czyli wstępnie skompilowanej plik interfejsu (patrz dokumentacja ). .gpiPliki te są generowane przez kompilator wraz z .oplikami podczas kompilacji każdego modułu. Tak więc symbol globalny MyStaticVariablejest zdefiniowany tylko raz w pliku obiektowym a.o.

Java działa w podobny sposób: kiedy następnie kompilator importuje klasę A do klasy B, szuka pliku klasy dla A i nie potrzebuje go A.java. Tak więc wszystkie definicje i inicjalizacje dla klasy A można umieścić w jednym pliku źródłowym.

Wracając do C ++, powód, dla którego w C ++ musisz zdefiniować statyczne elementy danych w osobnym pliku, jest bardziej związany z modelem kompilacji C ++ niż z ograniczeniami narzuconymi przez linker lub inne narzędzia używane przez kompilator. W C ++ import niektórych symboli oznacza zbudowanie ich deklaracji jako części bieżącej jednostki kompilacji. Jest to bardzo ważne między innymi ze względu na sposób kompilowania szablonów. Oznacza to jednak, że nie można / nie należy definiować żadnych globalnych symboli (funkcji, zmiennych, metod, elementów danych statycznych) w dołączonym pliku, w przeciwnym razie symbole te mogłyby zostać wielokrotnie zdefiniowane w skompilowanych plikach obiektowych.

Giorgio
źródło
42

Ponieważ elementy statyczne są współużytkowane przez WSZYSTKIE instancje klasy, muszą być zdefiniowane w jednym i tylko jednym miejscu. Naprawdę, są to zmienne globalne z pewnymi ograniczeniami dostępu.

Jeśli spróbujesz zdefiniować je w nagłówku, zostaną one zdefiniowane w każdym module, który zawiera ten nagłówek, a podczas łączenia wystąpią błędy podczas wyszukiwania wszystkich duplikatów definicji.

Tak, przynajmniej częściowo jest to kwestia historyczna pochodząca z frontu; mógłby zostać napisany kompilator, który stworzyłby rodzaj ukrytego „static_members_of_everything.cpp” i link do tego. Jednak złamałoby to kompatybilność wsteczną i nie przyniosłoby to żadnej realnej korzyści.

mjfgates
źródło
2
Moje pytanie nie jest powodem obecnego zachowania, ale uzasadnieniem takiej gramatyki języka. Innymi słowy, załóżmy, że jeśli staticzmienne są zadeklarowane / zdefiniowane w tym samym miejscu (jak Java), to co może pójść nie tak?
iammilind
8
@iammilind Myślę, że nie rozumiesz, że gramatyka jest konieczna z powodu wyjaśnienia tej odpowiedzi. Teraz dlaczego? Ze względu na model kompilacji C (i C ++): pliki c i cpp są prawdziwymi plikami kodu, które są kompilowane osobno jak osobne programy, a następnie są łączone ze sobą, aby uzyskać pełny plik wykonywalny. Nagłówki nie są tak naprawdę kodem kompilatora, są tylko tekstem do skopiowania i wklejenia do plików c i cpp. Teraz, jeśli coś jest zdefiniowane kilka razy, nie może go skompilować, w ten sam sposób, w jaki nie skompiluje się, jeśli masz kilka zmiennych lokalnych o tej samej nazwie.
Klaim,
1
@Klaim, a co z staticczłonkami template? Są dozwolone we wszystkich plikach nagłówka, ponieważ muszą być widoczne. Nie kwestionuję tej odpowiedzi, ale nie pasuje ona również do mojego pytania.
iammilind
Szablony @iammilind nie są prawdziwym kodem, to kod generujący kod. Każda instancja szablonu ma jedną i tylko jedną instancję statyczną każdej deklaracji statycznej dostarczonej przez kompilator. Nadal musisz zdefiniować instancję, ale podczas definiowania szablonu instancji nie jest to prawdziwy kod, jak powiedziano powyżej. Szablony to dosłownie szablony kodu kompilatora do generowania kodu.
Klaim,
2
@iammilind: Szablony są zazwyczaj tworzone w każdym pliku obiektowym, w tym w ich zmiennych statycznych. W systemie Linux z plikami obiektowymi ELF kompilator oznacza instancje jako słabe symbole , co oznacza, że ​​linker łączy wiele kopii tej samej instancji. Tej samej technologii można użyć do definiowania zmiennych statycznych w plikach nagłówków, więc przyczyną tego nie jest prawdopodobnie kombinacja przyczyn historycznych i rozważań dotyczących wydajności kompilacji. Mam nadzieję, że cały model kompilacji zostanie naprawiony, gdy następny standard C ++ będzie zawierał moduły .
han
6

Prawdopodobnym powodem jest to, że dzięki temu język C ++ można wdrożyć w środowiskach, w których plik obiektowy i model powiązań nie obsługują łączenia wielu definicji z wielu plików obiektowych.

Deklaracja klasy (zwana deklaracją z ważnych powodów) zostaje wciągnięta do wielu jednostek tłumaczeniowych. Jeśli deklaracja zawiera definicje zmiennych statycznych, wówczas otrzymujesz wiele definicji w wielu jednostkach tłumaczeniowych (i pamiętaj, że te nazwy mają powiązanie zewnętrzne).

Taka sytuacja jest możliwa, ale wymaga od linkera obsługi wielu definicji bez narzekania.

(Należy pamiętać, że jest to sprzeczne z regułą jednej definicji, chyba że można tego dokonać zgodnie z rodzajem symbolu lub sekcją, w której jest umieszczony).

Kaz
źródło
6

Istnieje ogromna różnica między C ++ a Javą.

Java działa na własnej maszynie wirtualnej, która tworzy wszystko we własnym środowisku wykonawczym. Jeśli definicja zostanie wyświetlona więcej niż jeden raz, po prostu zadziała na ten sam obiekt, o którym środowisko wykonawcze wie najlepiej.

W C ++ nie ma „ostatecznego właściciela wiedzy”: C ++, C, Fortran Pascal itp. Są „tłumaczami” z kodu źródłowego (pliku CPP) na format pośredni (plik OBJ lub plik „.o”, w zależności od OS), w którym instrukcje są tłumaczone na instrukcje maszynowe, a nazwy stają się adresami pośrednimi, w których pośredniczy tablica symboli.

Program nie jest tworzony przez kompilator, ale przez inny program („linker”), który łączy wszystkie OBJ-y razem (bez względu na język, z którego pochodzą) poprzez przekierowanie wszystkich adresów skierowanych w stronę symboli, w kierunku ich skuteczna definicja.

Przy tym, jak działa linker, definicja (która tworzy fizyczną przestrzeń dla zmiennej) musi być unikalna.

Zauważ, że C ++ sam w sobie nie łączy, a linker nie jest wydawany przez specyfikacje C ++: linker istnieje ze względu na sposób budowania modułów systemu operacyjnego (zwykle w C i ASM). C ++ musi go używać tak, jak jest.

Teraz: plik nagłówka można „wkleić” do kilku plików CPP. Każdy plik CPP jest tłumaczony niezależnie od każdego innego. Kompilator tłumaczący różne pliki CPP, wszystkie odbierające w tej samej definicji umieści „ kod tworzenia ” dla zdefiniowanego obiektu we wszystkich wynikowych OBJ.

Kompilator nie wie (i nigdy nie będzie wiedział), czy wszystkie te OBJ zostaną kiedykolwiek użyte razem do utworzenia jednego programu lub osobno do utworzenia różnych niezależnych programów.

Linker nie wie, w jaki sposób i dlaczego istnieją definicje i skąd pochodzą (nawet nie wie o C ++: każdy „statyczny język” może tworzyć definicje i odniesienia do połączenia). Po prostu wie, że istnieją odniesienia do danego „symbolu”, który jest „zdefiniowany” pod danym adresem wynikowym.

Jeśli dla danego symbolu istnieje wiele definicji (nie myl definicji z odniesieniami), linker nie ma wiedzy (bez względu na język) na temat tego, co z nimi zrobić.

To tak, jakby połączyć kilka miast i stworzyć duże miasto: jeśli okaże się, że masz dwa „ Plac Czasu ” i pewną liczbę osób przybywających z zewnątrz, proszących o przejście na „ Plac Czasu ”, nie możesz zdecydować na podstawie czysto technicznej (bez wiedzy na temat polityki, która przypisała te nazwiska i będzie odpowiedzialna za zarządzanie nimi), w którym dokładnym miejscu je wysłać.

Emilio Garavaglia
źródło
3
Różnica między Javą a C ++ w odniesieniu do symboli globalnych nie jest związana z tym, że Java ma maszynę wirtualną, ale raczej z modelem kompilacji C ++. Pod tym względem nie umieszczałbym Pascala i C ++ w tej samej kategorii. Zamiast tego zgrupowałbym C i C ++ razem jako „języki, w których importowane deklaracje są zawarte i skompilowane razem z głównym plikiem źródłowym”, w przeciwieństwie do Java i Pascala (a może OCaml, Scala, Ada itp.) Jako „języki, w których importowane deklaracje są wyszukiwane przez kompilator we wstępnie skompilowanych plikach zawierających informacje o eksportowanych symbolach ”.
Giorgio
1
@Giorgio: odniesienie do Javy może nie być mile widziane, ale myślę, że odpowiedź Emilio jest w większości właściwa, przechodząc do sedna problemu, mianowicie fazy pliku obiektowego / linkera po oddzielnej kompilacji.
ixache
5

Jest to wymagane, ponieważ w przeciwnym razie kompilator nie wie, gdzie umieścić zmienną. Każdy plik CPP jest indywidualnie kompilowany i nie wie o drugim. Linker rozwiązuje zmienne, funkcje itp. Osobiście nie widzę różnicy między elementami vtable i statycznymi (nie musimy wybierać, w jakim pliku są zdefiniowane vtable).

W większości zakładam, że pisarzom kompilatorów łatwiej jest to zaimplementować. Istnieją zmienne statyczne poza klasą / strukturą i być może albo ze względu na spójność, albo dlatego, że dla twórców kompilatorów łatwiej byłoby je zaimplementować, zdefiniowali to ograniczenie w standardach.


źródło
2

Myślę, że znalazłem powód. Zdefiniowanie staticzmiennej w oddzielnej przestrzeni pozwala na zainicjowanie jej do dowolnej wartości. Jeśli nie zostanie zainicjowany, domyślnie zostanie ustawiony na 0.

Przed C ++ 11 inicjalizacja w klasie była niedozwolona w C ++. Nie można więc pisać w następujący sposób:

struct X
{
  static int i = 4;
};

Dlatego teraz, aby zainicjować zmienną, należy zapisać ją poza klasą jako:

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

Jak omówiono również w innych odpowiedziach, int X::ijest teraz globalny, a deklarowanie globalności w wielu plikach powoduje błąd wielu łączy symboli.

Dlatego należy zadeklarować staticzmienną klasy w oddzielnej jednostce tłumaczenia. Jednak nadal można argumentować, że następujący sposób powinien poinstruować kompilator, aby nie tworzył wielu symboli

static int X::i = 4;
^^^^^^
iammilind
źródło
0

A :: x jest tylko zmienną globalną, ale przestrzeń nazw ma A i ma ograniczenia dostępu.

Ktoś nadal musi to zadeklarować, jak każda inna zmienna globalna, a można to zrobić nawet w projekcie, który jest statycznie powiązany z projektem zawierającym resztę kodu A.

Nazwałbym to wszystko złym projektem, ale istnieje kilka funkcji, które można wykorzystać w ten sposób:

  1. kolejność wywołań konstruktora ... Nie ważne dla int, ale dla bardziej złożonego elementu, który może uzyskiwać dostęp do innych zmiennych statycznych lub globalnych, może być krytyczny.

  2. inicjalizator statyczny - możesz pozwolić klientowi zdecydować, na co należy zainicjować A :: x.

  3. w c ++ i c, ponieważ masz pełny dostęp do pamięci za pomocą wskaźników, fizyczna lokalizacja zmiennych jest znacząca. Są bardzo niegrzeczne rzeczy, które można wykorzystać w zależności od tego, gdzie zmienna znajduje się w obiekcie łącza.

Wątpię, by to „dlaczego” zaistniała taka sytuacja. Prawdopodobnie jest to tylko ewolucja C zmieniającej się w C ++ i problem kompatybilności wstecznej, który powstrzymuje cię przed zmianą języka.

James Podesta
źródło
2
wydaje się, że nie oferuje to nic istotnego w porównaniu z punktami przedstawionymi i wyjaśnionymi w poprzednich 6 odpowiedziach
komnata