2010-12-28 9 views
42


Dla statycznych zmiennych członków w klasie C++ inicjalizacja odbywa się poza klasą. Zastanawiam się dlaczego? Jakiekolwiek logiczne uzasadnienie/ograniczenie tego? Czy jest to czysto starsza implementacja - której standard nie chce poprawić?Statyczna zmienna składowa C++ i jej inicjalizacja

Myślę, że inicjalizacja w klasie jest bardziej "intuicyjna" i mniej dezorientująca. Daje również poczucie zarówno statycznej, jak i globalnej wartości zmiennej. Na przykład, jeśli widzisz statyczny element const.

Odpowiedz

36

Zasadniczo jest tak, ponieważ statyczne elementy muszą być zdefiniowane w dokładnie jednej jednostce tłumaczeniowej, aby nie naruszać One-Definition Rule. Jeśli język zezwolą coś takiego:

struct Gizmo 
{ 
    static string name = "Foo"; 
}; 

następnie name byłoby zdefiniowane w każdej jednostce tłumaczeniowej #include ów ten plik nagłówka.

C++ pozwala na zdefiniowanie integralnych członów statycznych w deklaracji, ale nadal trzeba dołączyć definicję w ramach jednej jednostki tłumaczeniowej, ale jest to tylko skrót lub cukier syntaktyczny. Tak, jest to dozwolone: ​​

struct Gizmo 
{ 
    static const int count = 42; 
}; 

tak długo, jak: a) wyrażenie jest const integralną lub wyliczenie typ, b) wyrażenie może być ocenione w czasie kompilacji, oraz c) nie jest jeszcze gdzieś definicja, że ​​nie robi „t naruszają zasadę jedna definicja:

file: gizmo.cpp

#include "gizmo.h" 

const int Gizmo::count; 
+0

Reguła jednej definicji to: "Żadna jednostka tłumaczeniowa nie może zawierać więcej niż jednej definicji żadnej zmiennej, funkcji, typu klasy, typu wyliczeniowego lub szablonu". Jeśli Twój pierwszy przykład "Gizmo" byłby legalny, nie sądzę, że naruszałby zasadę One Definition, ponieważ każda jednostka tłumaczeniowa * miałaby * jedną definicję "Gizmo :: name". –

+0

@ Daniel Trebbien: To nie jest cała ODR. To tylko 3,2/1 - pierwsza szorstka warstwa "warstwy" ODR (aby zadbać o najbardziej oczywiste naruszenia). Pełna ODR zawiera bardziej szczegółowy zestaw wymagań dla każdego rodzaju jednostki. W przypadku obiektów z zewnętrznym łączem (jak również z funkcjami połączeń zewnętrznych), ODR jest dodatkowo ograniczony w 3.2/3 do jednej i jedynej definicji * dla całego programu *. – AnT

+1

@ Daniel Trebbien: Powodem, dla którego wymóg 3.2/1 został oddzielony od reszty, jest to, że naruszenie 3.2/1 wymaga diagnostyki z kompilatora, podczas gdy w przypadku naruszenia 3.2/3 diagnostyka nie jest wymagana. – AnT

2

Dzieje się tak ze względu na sposób kompilacji kodu. Jeśli miałbyś zainicjować go w klasie, która często znajduje się w nagłówku, za każdym razem, gdy nagłówek jest włączony, otrzymasz instancję zmiennej statycznej. To zdecydowanie nie jest intencja. Zainicjowanie go poza klasą daje możliwość zainicjowania go w pliku cpp.

+7

Jest to coś, co nowoczesna kombinacja kompilator/linker może z łatwością rozwiązać, a nie wystarczająco dobry powód do tak uciążliwego ograniczenia. – martona

+0

@martona ma rację. Łącznik C++ jest w stanie rozwiązać wiele definicji funkcji składowych, więc dlaczego nie statyczne zmienne składowe? O to właśnie chodzi w OP, tak myślę. –

+0

Sądzę, że tylko nowoczesne łączniki C++ mogłyby rozwiązać wiele definicji metod (funkcji składowych). (Tj. Ostatni raz próbowałem mieć wiele definicji metody przed wielu laty i połączenie nie powiodło się.) Wcześniej wszystkie metody zdefiniowane w nagłówku musiały być liniowe lub statyczne, a te ostatnie tworzyły wiele kopii w połączonych plik. –

0

Myślę, że głównym powodem inicjalizacji poza blokiem class jest umożliwienie inicjalizacji z zwracanymi wartościami innych funkcji składowych klasy. Jeśli chcesz zainicjować a::var z b::some_static_fn(), musisz najpierw upewnić się, że każdy plik .cpp zawierający a.h zawiera b.h. Byłby to bałagan, zwłaszcza gdy (prędzej czy później) natrafia się na okólnik, który można rozwiązać tylko wtedy, gdy nie jest konieczne niepotrzebne interface. Ten sam problem jest głównym powodem implementacji funkcji członka klasy w pliku .cpp zamiast umieszczania wszystkiego w głównej klasie ".h.

Przynajmniej z funkcjami członków masz możliwość zaimplementowania ich w nagłówku. W przypadku zmiennych musisz wykonać inicjalizację w pliku .cpp. Nie całkiem zgadzam się z tym ograniczeniem i nie sądzę, aby istniał ku temu dobry powód.

11

W C++ od początku czasów obecność inicjatora był wyłącznym atrybutem obiektu definicja, tj. Deklaracja z inicjatorem jest zawsze definicją (prawie zawsze).

Każdy zewnętrzny obiekt używany w programie C++ musi być zdefiniowany tylko raz tylko w jednej jednostce tłumaczeniowej.Zezwolenie na inicjowanie w klasie obiektów statycznych byłoby natychmiast sprzeczne z tą konwencją: inicjalizatory trafiałyby do plików nagłówkowych (gdzie zwykle znajdują się definicje klas), a tym samym generowały wiele definicji tego samego statycznego obiektu (po jednym dla każdej jednostki tłumaczeniowej zawierającej plik nagłówkowy). Jest to oczywiście niedopuszczalne. Z tego powodu metoda deklaracji dla statycznych członków klasy pozostaje idealnie "tradycyjna": tylko deklaruje w pliku nagłówkowym (tj. Bez inicjalizatora), a następnie definiuje to w wybranej jednostce tłumaczeniowej (ewentualnie z inicjatorem).

Jeden wyjątek od tej zasady został wprowadzony dla stałych członków klasy statycznej typu całkowego lub wyliczeniowego, ponieważ takie wpisy mogą być dla całkowych wyrażeń stałych (ICE). Główną ideą ICE jest to, że są one oceniane w czasie kompilacji, a zatem nie zależą od definicji zaangażowanych obiektów. Dlatego właśnie ten wyjątek był możliwy dla typów całkowych lub wyliczeniowych. Ale dla innych typów byłoby to po prostu sprzeczne z podstawowymi zasadami deklaracji/definicji C++.

1

Sekcja 9.4.2, statycznymi członkami danych, od C++ standardowych stanów:

Jeżeli członek static danych jest const integralnej lub const wyliczenia typu, jego oświadczenie w definicji klasy można określić const -initializator, który będzie integralnym wyrażeniem stałym.

Dlatego możliwe jest uwzględnienie wartości statycznego elementu danych "w klasie" (przez co rozumiem, że ma to znaczenie w deklaracji klasy). Jednak typ statycznego elementu danych musi być typem całkowania const lub const. Powodem, dla którego wartości statycznych elementów danych innych typów nie można określić w deklaracji klasy, jest to, że nietrywialna inicjalizacja jest prawdopodobnie wymagana (to znaczy, że konstruktor musi działać).

Wyobraźcie jeśli byli prawna:

// my_class.hpp 
#include <string> 

class my_class 
{ 
public: 
    static std::string str = "static std::string"; 
//... 

Każdy plik obiekt odpowiadający plików CPP, które zawierają ten nagłówek nie tylko kopię miejsca dla my_class::str (obejmującej sizeof(std::string) bajtów), ale również "sekcję ctor", która wywołuje konstruktor std::string, pobierając ciąg znaków C. Każda kopia przestrzeni do przechowywania dla my_class::str byłaby identyfikowana przez wspólną etykietę, więc łącznik teoretycznie może połączyć wszystkie kopie przestrzeni pamięci w pojedynczą. Jednak linker nie będzie w stanie wyizolować wszystkich kopii kodu konstruktora w sekcjach ctor plików obiektów. To tak, jakby prosząc łącznik, aby usunąć cały kod do zainicjowania str w kompilacji z następujących powodów:

std::map<std::string, std::string> map; 
std::vector<int> vec; 
std::string str = "test"; 
int c = 99; 
my_class mc; 
std::string str2 = "test2"; 

EDIT Pouczające jest patrzeć na wyjściu montera g ++ za pomocą następującego kodu:

// SO4547660.cpp 
#include <string> 

class my_class 
{ 
public: 
    static std::string str; 
}; 

std::string my_class::str = "static std::string"; 

Kod zespół może być uzyskane przez wykonanie:

g++ -S SO4547660.cpp 

patrząc przez SO4547660.s plik generowany przez g ++, można zauważyć, że istnieje dużo kodu dla tak małego pliku źródłowego.

__ZN8my_class3strE to etykieta miejsca do przechowywania my_class::str.Istnieje również źródło zespołu funkcji __static_initialization_and_destruction_0(int, int), która ma etykietę __Z41__static_initialization_and_destruction_0ii. Ta funkcja jest wyjątkowa dla g ++, ale wystarczy wiedzieć, że g ++ upewni się, że zostanie wywołana przed wykonaniem jakiegokolwiek kodu nieinicjalizującego. Zauważ, że implementacja tej funkcji wywołuje __ZNSsC1EPKcRKSaIcE. To jest zniekształcony symbol dla std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&).

Wracając do hipotetycznym przykładzie powyżej i stosując te szczegóły, każdy plik obiekt odpowiadający pliku CPP, który zawiera my_class.hpp miałby etykietę __ZN8my_class3strE dla sizeof(std::string) bajtów, a także kod montaż zadzwonić __ZNSsC1EPKcRKSaIcE w jego realizacji __static_initialization_and_destruction_0(int, int) funkcjonować. Łącznik może z łatwością łączyć wszystkie wystąpienia __ZN8my_class3strE, ale nie może wyizolować kodu, który wywołuje __ZNSsC1EPKcRKSaIcE w implementacji pliku obiektu __static_initialization_and_destruction_0(int, int).

+0

Dlaczego więc następujące niedozwolone: ​​'class my_class {public: static const double pi = 3.14; }; ' –

+0

@John: Myślę, że powinno to być dozwolone z tego samego powodu, dla którego wartości statycznych elementów danych typu" const' integer lub "const" mogą być określone za pomocą deklaracji. Nie wiem, dlaczego tak nie jest. –

+0

Sugeruje to, że "nietrywialna" inicjalizacja może nie być jedynym powodem, dla którego nie jest dozwolona dla typów nie-integralnych. –