[10] Konstruktory
(Część C++ FAQ Lite, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)


FAQ - sekcja [10]:


[10.1] Co to są konstruktory?

Konstruktory tworzą obiekty z niczego.

Konstruktory są to funkcje wywoływane podczas tworzenia nowego obiektu i służące do jego inicjalizacji (przygotowaniu do późniejszego użycia). Przeobrażają one stertę bitów w pamięci w nowy, "żywy" obiekt. W najprostszym przypadku konstruktor ustawia pola obiektu na pewne wartości początkowe. Na ogół alokuje również pewne zasoby systemowe (pamięć, pliki, semafory, gniazda, itd) [to tylko przykłady, w praktyce nie ma żadnego ograniczenia na to co konstruktor ma zrobić w czasie tworzenia obiektu, podobnie jak w jakiejkolwiek innej funkcji - przyp. tłum.].

W języku angielskim zamiast słowa "constructor" często stosuje się skrót "ctor".

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.2] Czy jest jakaś różnica między zapisem List x; a List x();?

Jest, i to duża!

Załóżmy, że List jest nazwą jakiejś klasy. W poniższym przykładzie funkcja f() deklaruje lokalny obiekt x klasy List.

 void f()
 {
   List x;     
// Lokalny obiekt x (klasa List)
   
// ...
 }

Jednak w kolejnym przykładzie funkcja g() deklaruje funkcję x() zwracającą obiekt klasy List.

 void g()
 {
   List x();   
// Funkcja x (typem zwracanej wartości jest List)
   
// ...
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.3] Czy konstruktor pewnej klasy może wywołać inny konstruktor należący do tej samej klasy, tak aby zainicjował ten sam obiekt? UPDATED!

[Recently napisany ponownie w celu dokładniejszego wyjaśnienia, dziękuję za uwagi Nikolai Borissov (in 9/02) and Wesja polska - poprawiono drugi akapit, aby zbliżyć go bardziej do oryginału (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Nie.

Zobaczmy to na przykładzie. Przypuśćmy, że chcesz aby twój konstruktor Foo::Foo(char) wywołał inny konstruktor tej samej klasy, np. Foo::Foo(char,int), tak aby ten drugi pomógł w inicjalizacji tego samego obiektu (obiektu widzianego przez pierwszy konstruktor jako this). Niestety, nie ma możliwości zrobienia tego w C++.

Pomimo tego niektórzy ludzie i tak próbują wykonać tą operację, niestety nie osiągając zamierzonego celu. Na przykład, instrukcja Foo(x, 0); nie wywołuje konstruktora Foo::Foo(char,int) na obiekcie this. Konstruktor ten zostanie wywołany, jednakże w celu utworzenia nowego, lokalnego obiektu (nie this), który zostanie natychmiast zniszczony wraz z przejściem do następnej instrukcji za średnikiem ; [obiektowi nie nadano nazwy, stąd jego zasięg związany jest jedynie z instrukcją, w której został utworzony - przyp. tłum.].

 class Foo {
 public:
   Foo(char x);
   Foo(char x, int y);
   ...
 };
 
 Foo::Foo(char x)
 {
   ...
   Foo(x, 0);  
// to NIE pomaga w inicjalizacji obiektu this!!
   ...
 }

Czasami można połączyć dwa konstruktory, dzięki możliwości nadawania parametrom domyślnych wartości w języku C++.

 class Foo {
 public:
   Foo(char x, int y=0);  
// połączenie dwóch konstruktorów
   ...
 };

Nie zawsze ten sposób nadaje się do połączenia kilku konstruktorów (np. gdy nie da się odpowiednio dobrać parametrów i ich domyślnych wartości). W takich przypadkach czasami można przenieść wspólną część kodu konstruktorów do osobnej, prywatnej metody. W poniższym przykładzie taką rolę spełnia metoda init():

 class Foo {
 public:
   Foo(char x);
   Foo(char x, int y);
   ...
 private:
   void init(char x, int y);
 };
 
 Foo::Foo(char x)
 {
   init(x, int(x) + 7);
   ...
 }
 
 Foo::Foo(char x, int y)
 {
   init(x, y);
   ...
 }
 
 void Foo::init(char x, int y)
 {
   ...
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.4] Czy domyślnym konstruktorem dla klasy Fred jest zawsze Fred::Fred()? UPDATED!

[Recently częściowo przepisany dla lepszego wyjaśnienia tematu (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Nie. "Domyślny konstruktor" to ten, który może zostać wywołany bez podawania parametrów. Jednym z przykładów może być konstruktor nie pobierający parametrów.

 class Fred {
 public:
   Fred();   
// Domyślny konstruktor: może zostać wywołany bez parametrów
   
// ...
 };

"Domyślnym konstruktorem" może być również konstruktor, w którym wszystkie parametry mają podane domyślne wartości.

 class Fred {
 public:
   Fred(int i=3, int j=5);   
// Domyślny konstruktor: może zostać wywołany bez
      
// parametrów
   
// ...
 };

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.5] Który konstruktor zostanie wywołany, jeśli utworzę tablicę obiektów klasy Fred? UPDATED!

[Recently Wersja polska - usunięto błędny przypis tłumacza sprzed ostatniego akapitu (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Domyślny konstruktor klasy Fred (z wyjątkiem przypadków omówionych poniżej).

Nie ma możliwości wymuszenia na kompilatorze, aby ten zastosował wywołanie innego konstruktora (z wyjątkiem opisanych dalej sytuacji). Jeżeli klasa Fred nie posiada domyślnego konstruktora, próba utworzenia tablicy obiektów tej klasy spowoduje błąd podczas kompilacji.

 class Fred {
 public:
   Fred(int i, int j);
   
// ... zakładamy, że nie ma domyślnego konstruktora
   
// w klasie Fred ...
 };
 
 int main()
 {
   Fred a[10];               
// BŁĄD: Fred nie posiada domyślnego
      
// konstruktora
   Fred* p = new Fred[10];   
// BŁĄD: Fred nie posiada domyślnego
      
// konstruktora
 }

Jednakże, jeśli nie konstruujesz "zwykłej tablicy", tylko np. wektor std::vector<Fred> (co w zasadzie powinieneś robić, gdyż tablice są złe), to klasa Fred nie musi zawierać domyślnego konstruktora, ponieważ wektor std::vector tworzy się w oparciu o gotowy obiekt, który może zostać zainicjowany dowolnym konstruktorem:

 #include <vector>
 
 int main()
 {
   std::vector<Fred> a(10, Fred(5,7));
   
// 10 obiektów Fred w wektorze a typu std::vector zostanie
   
// zainicjowanych według obiektu Fred(5,7).
   
// ...
 }

Pomimo tego, że generalnie powinno się używać wektorów typu std::vector zamiast tablic, zdarzają się takie przypadki, gdy tablica wydaje się być właściwym rozwiązaniem - dla takich przypadków istnieje "jawna inicjalizacja elementów tablicy". Wygląda to tak:

 class Fred {
 public:
   Fred(int i, int j);
   
// ... zakładamy, że nie ma domyślnego konstruktora
   
// w klasie Fred ...
 };
 
 int main()
 {
   Fred a[10] = {
     Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7),
     Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7)
   };
 
   
// 10 obiektów klasy Fred w tablicy a zostanie zainicjowanych według
   
// obiektu Fred(5,7).
   
// ...
 }

Oczywiście, nie wszystkie elementy muszą zawierać Fred(5,7 — możesz w nich umieścić dowolne inne wartości. Chodzi o to, że powyższy zapis jest (a) użyteczny ale (b) nie tak wygodny jak w przypadku std::vector. Zapamiętaj: tablice są złe — z wyjątkiem sytuacji, w których istnieje naprawdę niepodważalny argument za użyciem tablic, zawsze używaj wektora std::vector.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.6] Czy moje konstruktory powinny używać "list inicjujących" czy "przypisań"? UPDATED!

[Recently dodano argument za stosowaniem list inicjujących, związany z nie-statycznymi, stałymi składowymi; dziękuję za uwagę Tanmoy Bhattacharya (in 6/02). Click here to go to the next FAQ in the "chain" of recent changes.]

List inicjujących. W zasadzie, konstruktory powinny inicjować wszystkie składowe przy użyciu listy inicjującej.

Przykładowo, ten konstruktor inicjalizuje składową x_ przy użyciu listy inicjującej: Fred::Fred() : x_(cokolwiek) { }. Główną korzyścią wynikającą z tego rozwiązania jest zwiększenie wydajności. Np., jeśli wyrażenie cokolwiek jest tego samego typu co składowa x_, wynik wyrażenia cokolwiek może zostać utworzony bezpośrednio wewnątrz składowej x_ — kompilator nie będzie musiał tworzyć osobnej kopii obiektu, która została by później, po obliczeniu wyrażenia, przepisana do składowej x_. Ale nawet jeśli typy nie są zgodne, kompilator zazwyczaj jest w stanie wykonać lepszą pracę w przypadku list inicjujących niż przypisań.

Innym (gorszym pod względem wydajności) sposobem tworzenia konstruktorów jest użycie przypisań, na przykład: Fred::Fred() { x_ = cokolwiek ; }. W tym przypadku wyrażenie cokolwiek stanowi osobny, tymczasowy obiekt, który musi zostać utworzony i przekazany (po obliczeniu wyrażenia) do funkcji obsługującej operator przypisania dla składowej x_. Następnie ten tymczasowy obiekt zostaje usunięty w punkcie ;. Takie rozwiązanie nie jest wydajne.

Gdyby tego było mało, jest jeszcze jedna przyczyna gorszej wydajności przy stosowaniu przypisań: składowa x_ jest tworzona całkowicie od nowa przez swój domyślny konstruktor - konstruktor ten może np. alokować część pamięci lub otwierać jakiś plik. Wysiłek ten może pójść na marne, jeśli wyrażenie cokolwiek i/lub operator przypisania "zmuszą" składową do zamknięcia tego pliku lub uwolnienia pamięci (np. jeśli domyślnemu konstruktorowi nie uda się alokacja wystarczająco dużego obszaru lub otworzy on niewłaściwy plik).

Konkludując: Przy tym samym efekcie końcowym, szybkość Twojego kodu wzrośnie, jeśli będziesz używać list inicjujących zamiast przypisań.

Notka: W obu przypadkach nie będzie żadnej różnicy wydajności, jeżeli składowa x_ jest typu podstawowego/wbudowanego, takiego jak int, char* lub float. Osobiście, mimo wszystko wolę inicjować takie składowe przy użyciu list inicjujących, niż przypisań, dla konsekwencji w zapisie. Jest również inny argument za stosowaniem list inicjujących nawet w przypadku typów podstawowych/wbudowanych: nie-statyczne stałe składowe nie mogą mieć przypisanej nowej wartości wewnątrz definicji konstruktora [można tego dokonać tylko przy użyciu listy inicjującej - przyp. tłum.], tak więc inicjalizowanie wszystkiego przy użyciu listy inicjującej nabiera głębszego sensu.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.7] Czy powinno się używać wskaźnika this wewnątrz konstruktora?

Niektórzy uważają, że nie powinno się używać wskaźnika this w obrębie konstruktora, ponieważ obiekt nie został jeszcze utworzony do końca. Mimo tego możesz używać wskaźnika this w konstruktorze (zarówno w jego {definicji} jak i w liście inicjującej) o ile zachowasz pewną ostrożność.

Przykład czegoś, co zawsze działa: w obrębie {definicji} konstruktora (lub funkcji wywołanej przez niego) można bez problemu odwoływać się do składowych klasy bazowej oraz/lub do składowych klasy, do której konstruktor należy. Wynika to z faktu, że wszystkie te składowe napewno zostały w pełni skonstruowane przed wywołaniem konstruktora.

Przykład czegoś, co nigdy nie działa: w obrębie {definicji} konstruktora (lub funkcji wywołanej przez niego) nie można wywoływać metod wirtualnych przeciążonych w klasie "położonej niżej" (potomnej). Jeżeli Twoim celem byłoby dostanie się do metody przeciążonej w klasie potomnej, nie udałoby Ci się to [w takim przypadku nastąpi wywołanie tej wersji funkcji, która jest zdefiniowana w klasie do której należy konstruktor - przyp. tłum.]. Zwróć uwagę, że nie można wywołać wersji przeciążonej w klasie potomnej niezależnie od tego jak będziesz wywoływać tą wirtualną metodę: jawnie przy użyciu wskaźnika this (np. this->metoda(), niejawnie (np. metoda(), czy też nawet wywołując jakąś inną funkcję, która wywołuje metodę wirtualną tworzonego obiektu. Innymi słowy: nawet jeśli tworzony jest obiekt jakiejś klasy potomnej Y, to w czasie wykonywania konstruktora klasy podstawowej X obiekt jeszcze nie należy do klasy Y. Zostałeś ostrzeżony/a!

Przykład czegoś, co czasami działa: jeżeli przekazujesz składową A obiektu this do składowej B tego samego obiektu przy użyciu listy inicjującej, to musisz się upewnić, że składowa A została już zainicjowana [musi mieć ona jakąś wartość, żeby móc ją dalej przekazać - przyp. tłum.]. Dobrą wiadomością jest fakt, że na szczęście można określić, czy dana składowa jest już zainicjowana czy jeszcze nie, przy użyciu pewnych prostych reguł języka niezależnych od kompilatora. Zła wiadomość jest natomiast taka, że musisz znać wszystkie te reguły (np., składowe klasy podstawowej są inicjowane najpierw (sprawdź dokładnie kolejność, jeżeli masz do czynienia z wielokrotnym i/lub wirtualnym dziedziczeniem!), następnie składowe danej klasy inicjowane są w tej samej kolejności w jakiej są zadeklarowane wewnątrz klasy). Jeżeli nie znasz tych reguł to nie przekazuj w liście inicjującej żadnych składowych obiektu this (nie ważne, czy używasz wskaźnika this jawnie, czy niejawnie przy użyciu samej nazwy składowej)! A jeśli znasz reguły to, proszę, zachowaj ostrożność.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.8] Co to są "nazwane konstruktory" (ang. "Named Constructor Idiom")?

Jest to technika umożliwiająca użytkownikom klasy na bardziej intuicyjne i/lub bezpieczniejsze tworzenie jej obiektów.

Problem ze "zwykłymi" konstruktorami jest taki, że mają one tą samą nazwę, co klasa. Z tego powodu można je rozróżnić tylko dzięki różnym listom parametrów. Jeżeli konstruktorów pewnej klasy jest zbyt dużo, różnice między nimi stają się cokolwiek subtelne, co może stać się przyczyną różnych błędów.

Technika "Named Constructor Idiom" polega na tym, że wszystkie konstruktory deklaruje się jako prywatne lub chronione, a następnie definiuje się publiczne i statyczne metody, które tworzą i zwracają nowy obiekt [spełniają więc rolę konstruktora - przyp. tłum.]. Te statyczne metody są "nazwanymi konstruktorami" (ang. "Named Constructors") [ponieważ jako "zwykłe" metody mogą posiadać dowolne nazwy, inne niż nazwa klasy - przyp. tłum.]. Generalnie definiuje się po jednej takiej statycznej metodzie dla każdego sposobu na utworzenie obiektu danej klasy.

Na przykład, przypuśćmy, że projektujemy klasę Point reprezentującą punkt na płaszczyźnie X-Y. Okazuje się, że są dwa sposoby na podanie współrzędnych na takiej płaszczyźnie - można podać współrzędne prostokątne (X+Y) lub współrzędne biegunowe (promień+kąt). (Nie martw się, jeżeli nie przypominasz sobie tego; nie chodzi tu o szczegóły związane z rodzajami współrzędnych; chodzi o to, że istnieje kilka sposobów na utworzenie obiektu klasy Point.) Niestety, w obu przypadkach parametry są tych samych typów i tej samej ilości: dwie liczby float. Spowoduje to wystąpienie w czasie kompilacji błędu niejednoznaczności (ang. ambiguity error) przy przeciążaniu konstruktorów:

 class Point {
 public:
   Point(float x, float y);     
// Współrzędne prostokątne
   Point(float r, float a);     
// Współrzędne biegunowe (promień i kąt)
   
// BŁĄD: Przeciążenie jest niejednoznaczne: Point::Point(float,float)
 };
 
 int main()
 {
   Point p = Point(5.7, 1.2);   
// Niejednoznaczność: o który rodzaj
         
// współrzędnych chodzi?
 }

Jednym ze sposobów rozwiązania tej niejednoznaczności jest zastosowanie nazwanych konstruktorów:

 #include <cmath>               // funkcje sin() and cos()
 
 class Point {
 public:
   static Point rectangular(float x, float y);      
// Wsp. prostokątne
   static Point polar(float radius, float angle);   
// Wsp. biegunowe
   
// Powyższe statyczne metody to "nazwane konstruktory"
   
// ...
 private:
   Point(float x, float y);     
// "Właściwy" konstruktor stosuje współrzędne
   float x_, y_;        
// prostokątne.
 };
 
 inline Point::Point(float x, float y)
 : x_(x), y_(y) { }
 
 inline Point Point::rectangular(float x, float y)
 { return Point(x, y); }
 
 inline Point Point::polar(float radius, float angle)
 { return Point(radius*cos(angle), radius*sin(angle)); }

Teraz użytkownicy klasy Point mogą tworzyć jej obiekty przy użyciu przejrzystej i jednoznacznej składni, w obu systemach współrzędnych.

 int main()
 {
   Point p1 = Point::rectangular(5.7, 1.2);   
// Wsp. prostokątne
   Point p2 = Point::polar(5.7, 1.2);         
// Wsp. biegunowe
 }

Jeżeli masz zamiar tworzyć klasy potomne względem klasy Point to upewnij się, że "właściwe" konstruktory znajdują się w sekcji chronionej (pod słowem kluczowym protected).

Nazwane konstruktory mogą również służyć do zapewnienia, że obiekty danej klasy są zawsze tworzone przy użyciu instrukcji new.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.9] Dlaczego nie mogę zainicjować składowych statycznych w liście inicjującej konstruktora?

Ponieważ składowe statyczne muszą być zdefiniowane jawnie dla całej klasy.

Fred.h:

 class Fred {
 public:
   Fred();
   
// ...
 private:
   int i_;
   static int j_;
 };

Fred.cpp (lub Fred.C lub cokolwiek):

 Fred::Fred()
   : i_(10)  
// OK: można (a nawet trzeba) inicjować składowe w ten sposób
   , j_(42)  
// BŁĄD: nie można tak inicjować statycznych składowych
 {
   
// ...
 }
 
 
// Składowe statyczne muszą być definiowane w ten sposób:
 int Fred::j_ = 42;

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.10] Dlaczego klasy ze składowymi statycznymi są przyczyną błędów linkera?

Ponieważ składowe statyczne muszą być jawnie zdefiniowane w dokładnie jednym module podlegającym kompilacji (pliku) . Jeżeli tego nie zrobisz, linker prawdopodobnie zgłosi błąd "niezdefiniowany symbol zewnętrzny" (ang. "undefined external"). Na przykład:

 // Fred.h
 
 class Fred {
 public:
   
// ...
 private:
   static int j_;   
// Deklaruje składową statyczną Fred::j_
   
// ...
 };

Linker zbluzga Cię ("Nie zdefiniowano Fred::j_" - ang. "Fred::j_ is not defined") chyba że zdefiniujesz (a nie tylko zadeklarujesz, jak w powyższym przykładzie) składową Fred::j_ w (dokładnie) jednym z plików kodu źródłowego:

 // Fred.cpp
 
 #include "Fred.h"
 
 int Fred::j_ = jakieś_wyrażenie_dające_się_przekształcić_do_typu_int;
 
 
// Ewentualnie, jeśli chcesz niejawnie wpisać do składowej wartość 0:
 
// int Fred::j_;

Typowym miejscem do zdefiniowania statycznych pól klasy Fred jest plik Fred.cpp (lub Fred.C lub jakiegokolwiek rozszerzenia używasz).

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.11] Co to jest "problem kolejności inicjalizacji statycznych" (ang. "static initialization order fiasco")?

Jest to subtelny sposób "ubicia" Twojego programu.

Problem kolejności inicjalizacji statycznych jest bardzo subtelnym i często nierozumianym aspektem C++. Niestety, jest bardzo trudny do wykrycia — błąd występuje jeszcze przed rozpoczęciem wykonywania funkcji main().

W skrócie - załóżmy że mamy dwa statyczne obiekty x i y, które znajdują się w dwóch różnych plikach, powiedzmy x.cpp i y.cpp. Załóżmy następnie, że część inicjująca obiekt y (zazwyczaj konstruktor obiektu y) wywołuje jakąś metodę z obiektu x.

To wszystko. Prawda, że proste?

Tragedią całej sytuacji jest to, że masz szanse 50% na 50%, że Twój program "zdechnie". Jeżeli moduł skompilowany z pliku x.cpp zostanie zainicjowany wcześniej, wtedy wszystko jest OK. Ale jeżeli najpierw zostanie zainicjowany moduł skompilowany z pliku y.cpp, wtedy inicjalizacja y nastąpi przed inicjalizacją x i jesteś w potrzasku. Np. konstruktor y mógłby wywołać metodę z obiektu x, chociaż x nie został jeszcze skonstruowany.

Słyszałem, że McDonalds szuka nowych pracowników. Życzę miłej pracy przy przewracaniu burgerów.

Jeżeli uważasz granie w Rosyjską Ruletkę z magazynkiem wypełnionym do połowy za "ekscytujące", to możesz już nie czytać dalej. Z drugiej strony, jeśli wolisz zwiększać swoje szanse przeżycia przez uprzednie zapobieganie katastrofom, to prawdopodobnie chciał(a)byś przeczytać kolejnego faqa.

Notka: Problem kolejności inicjalizacji statycznych może w niektórych przypadkach, dotyczyć również typów wbudowanych/podstawowych.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.12] W jaki sposób mogę zapobiec problemowi kolejności inicjalizacji obiektów statycznych (ang. "static initialization order fiasco")?

Użyj metody "konstruuj przy pierwszym użyciu" (ang. "construct on first use idiom") - polega ona po prostu na umieszczeniu statycznego obiektu wewnątrz funkcji.

Na przykład, przypuśćmy że masz dwie klasy, Fred i Barney. Istnieją dwa globalne obiekty tych klas: x klasy Fred, i y klasy Barney. Konstruktor klasy Barney wywołuje metodę goBowling() obiektu x. Obiekt x jest zdefiniowany w pliku x.cpp:

 // Plik x.cpp
 #include "Fred.hpp"
 Fred x;

Natomiast obiekt y jest zdefiniowany w pliku y.cpp:

 // Plik y.cpp
 #include "Barney.hpp"
 Barney y;

Aby wszystko było jasne, konstruktor klasy Barney wygląda mniej więcej tak:

 // File Barney.cpp
 #include "Barney.hpp"
 
 Barney::Barney()
 {
   
// ...
   x.goBowling();
   
// ...
 }

Jak wyjaśniono powyżej, katastrofa nastąpi, jeśli y zostanie skonstruowany przed x, co zdarza się w 50% przypadków, gdyż oba obiekty są zdefiniowane w osobnych plikach.

Istnieje wiele rozwiązań tego problemu, ale bardzo prostym i całkowicie przenośnym sposobem jest zastąpienie globalnego obiektu x globalną funkcją x(), która zwraca referencję do obiektu klasy Fred.

 // Plik x.cpp - nowa wersja
 
 #include "Fred.hpp"
 
 Fred& x()
 {
   static Fred* ans = new Fred();
   return *ans;
 }

Ponieważ lokalne obiekty ze słowem static są konstruowane tylko za pierwszym razem, gdy zostaje wykonana linijka z ich definicją, instrukcja new Fred() zostanie wywołana tylko raz: przy pierwszym wywołaniu funkcji x(). W każdym kolejnym wywołaniu funkcja zwróci ten sam wcześniej utworzony obiekt (wskazywany przez ans). Wszystko co teraz pozostało do zrobienia to zamiana odwołań do obiektu x na wywołania funkcji x().

 // Plik Barney.cpp - nowa wersja
 #include "Barney.hpp"
 
 Barney::Barney()
 {
   
// ...
   x().goBowling();
   
// ...
 }

Rozwiązanie to nosi nazwę "konstruuj przy pierwszym użyciu" Construct On First Use Idiom, ponieważ dokładnie taki jest jej efekt: obiekt klasy Fred jest konstruowany przy pierwszym jego użyciu [przy pierwszym wywołaniu funkcji x() - przyp. tłum.].

Wadą tej metody jest to, że tak utworzony obiekt nigdy nie zostanie zniszczony [nie zostanie wywołany jego destruktor - przyp. tłum.]. Istnieje inna metoda rozwiązująca ten problem, ale może ona doprowadzić do powstania innego (równie złośliwego) problemu - stąd należy ją stosować z pewną ostrożnością.

Notka: Problem kolejności inicjalizacji statycznych może w niektórych przypadkach, dotyczyć również typów wbudowanych/podstawowych.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.13] Dlaczego w metodzie "konstruuj przy pierwszym użyciu" (ang. "construct-on-first-use idiom"] używa się statycznego wskaźnika, a nie statycznego obiektu? UPDATED!

[Recently wprowadzono istotne zmiany do drugiego i kolejnych akapitów, dziękuję za uwagę Amitha Perera i Wil Evers (in 6/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Krótsza odpowiedź: możliwe jest użycie statycznego obiektu zamiast statycznego wskaźnika, prowadzi to jednak do powstania innego (równie subtelnego i złośliwego) problemu.

Dłuższa odpowiedź: czasami ludzi martwi fakt, że poprzednie rozwiązanie doprowadza do "wycieku pamięci". W większości przypadków nie stanowi to problemu, jednak w niektórych owszem - stanowi. Zwróć uwagę: mimo, że obiekt wskazywany przez ans w poprzednim faqu, nie zostanie usunięty z pamięci, to żaden wyciek nie nastąpi. Jest to spowodowane tym, że system operacyjny - po zakończeniu programu - zwolni całą pamięć alokowaną w stercie programu. Innymi słowy, jest się o co martwić jedynie wtedy, gdy destruktor obiektu ma do wykonania jakieś ważne zadanie (np. zapisuje coś do pliku), które musi zostać wykonane wtrakcie zakończenia programu.

W takich przypadkach, gdy "skonstruowany przy pierwszym użyciu" obiekt (w naszym przykładzie Fred) musi zostać później zniszczony, warto rozważyć zmianę funkcji x() na następującą:

 // Plik x.cpp
 
 #include "Fred.hpp"
 
 Fred& x()
 {
   static Fred ans;  
// było static Fred* ans = new Fred();
   return ans;       
// było return *ans;
 }

Jednakże z tą zmianą wiąże się (lub raczej "może się wiązać") dosyć subtelny problem. Aby móc zrozumieć ten potencjalny problem, przypomnijmy sobie najpierw po co właściwie stosujemy te wszystkie sztuczki: mianowicie potrzebujemy mieć 100% pewności, że nasz statyczny obiekt (a) zostanie zainicjowany przed pierwszym odwołaniem do niego i (b) nie zostanie zniszczony przed jego ostatnim użyciem. Oczywiście, jeśli obiekt zostałby użyty przed jego skonstruowaniem lub po jego zniszczeniu, doprowadziłoby to do katastrofy. Wniosek jest taki, że musisz zadbać o to, aby nie nastąpiła żadna z tych dwóch sytuacji (statyczna inicjalizacja i statyczna deinicjalizacja), nie tylko jedna.

Po zmianie deklaracji static Fred* ans = new Fred(); na static Fred ans;, wciąż prawidłowo obsługujemy inicjalizację, ale pojawia się problem z deinicjalizacją. Na przykład, jeżeli mamy 3 statyczne obiekty, nazwijmy je a, b i c, których destruktory używają obiektu ans, to aby uniknąć katastrofy w trakcie statycznej deinicjalizacji, obiekt ans musi zostać zniszczony po pozostałych trzech.

Wniosek jest prosty: jeżeli istnieją jakiekolwiek inne statyczne obiekty, których destruktory mogą użyć obiektu ans po jego zniszczeniu, to BUM, nie żyjesz. Jeżeli konstruktory obiektów a, b i c używają obiektuans, wszystko powinno być OK, gdyż biblioteka czasu pracy programu (ang. runtime system), w ramach statycznej deinicjalizacji, zniszczy ans po tym, jak ostatni z tych trzech obiektów zostanie zniszczony. Jednak, jeśli konstruktorowi obiektu a i/lub b i/lub c nie powiedzie się próba użycia obiektu ans i/lub jeśli jakiś kod dostanie adres ansa i przekaże go do innego statycznego obiektu, musisz być bardzo, bardzo ostrożnym - w takiej sytuacji wszystko się może zdarzyć.

Istnieje jeszcze trzeci sposób, który rozwiązuje zarówno problem ze statyczną inicjalizacją jak i deinicjalizacją, ale ma on swoje wymagania, które są niemałe. Niestety, jestem zbyt leniwy (i zajęty!) aby napisać więcej faqów dzisiaj, więc jeśli interesuje Cię ten trzeci sposób, to musisz kupić sobie jakąś książkę, która go szczegółowo opisuje. Jedną z takich książek jest C++ FAQs; zawiera ona również analizę kosztów/zysków, która pozwoli Ci zdecydować czy i kiedy zastosować ten trzeci sposób.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.14] Jak mogę zapobiec problemowi kolejności inicjalizacji obiektów statycznych w przypadku statycznych pól klasy?

Po prostu użyj wcześniej opisanej techniki, ale tym razem zamiast statycznej funkcji globalnej zdefiniuj statyczną metodę.

Przypuśćmy, że masz klasę X która zawiera statyczny obiekt klasy Fred:

 // Plik X.hpp
 
 class X {
 public:
   
// ...
 
 private:
   static Fred x_;
 };

Naturalnie, ta statyczna składowa jest inicjalizowana oddzielnie:

 // Plik X.cpp
 
 #include "X.hpp"
 
 Fred X::x_;

Również naturalnie, obiekt klasy Fred będzie użyty w conajmniej jednej metodzie obiektu klasy X:

 void X::someMethod()
 {
   x_.goBowling();
 }

Tym razem "katastroficzny scenariusz" nastąpi, jeśli ktoś gdzieś jakoś wywoła tę metodę, zanim obiekt klasy Fred (x_) zostanie skonstruowany. Na przykład, jeśli ktoś inny utworzy statyczny obiekt klasy X i wywoła metodę someMethod() w trakcie inicjalizacji statycznej, wtedy znajdziesz się na łasce i niełasce kompilatora, gdyż to od niego będzie zależeć, czy zainicjuje on składową X:x_ przed czy po wywołaniu metody someMethod(). (Komitet ANSI/ISO ds. języka C++ pracuje nad tym problemem, jednak do tej pory nie są dostępne kompilatory, które by go jakoś rozwiązywały; jeśli sytuacja ta ulegnie zmianie to nie omieszkam o tym wzmiankować, tak więc proszę bacznie zwracać uwagę na wszelkie zmiany w tym miejscu).

Niezależnie od sytuacji, można zamienić pole X::x_ na metodę statyczną, co jest rozwiązaniem zarówno bezpiecznym jak i przenośnym:

 // Plik X.hpp
 
 class X {
 public:
   
// ...
 
 private:
   static Fred& x();
 };

Naturalnie, ta statyczna składowa jest inicjalizowana oddzielnie:

 // Plik X.cpp
 
 #include "X.hpp"
 
 Fred& X::x()
 {
   static Fred* ans = new Fred();
   return *ans;
 }

Następnie trzeba zamienić wszystkie wystąpienia x_ na x():

 void X::someMethod()
 {
   x().goBowling();
 }

Jeżeli zwracasz nadzwyczaj szczególną uwagę na wydajność i obawiasz się o koszty związane z wywołaniem nowej funkcji w metodzie X:someMethod(), możesz dodać lokalną statyczną referencję Fred&. Jak pamiętasz, lokalne statyczne obiekty są inicjalizowane tylko raz (przy pierwszym wykonaniu instrukcji zawierającej ich deklarację), zatem w poniższym przykładzie metoda X::x() zostanie wywołana tylko raz: w trakcie pierwszego wywołania metody X:someMethod():

 void X::someMethod()
 {
   static Fred& x = X::x();
   x.goBowling();
 }

Notka: Problem kolejności inicjalizacji statycznych może w niektórych przypadkach, dotyczyć również typów wbudowanych/podstawowych.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.15] Czy "problem kolejności inicjalizacji statycznych" może wystąpić również w przypadku wbudowanych/podstawowych typów danych?

Tak.

Jeżeli zainicjujesz zmienną typu wbudowanego/podstawowego przy użyciu wywołania funkcji, problem kolejności inicjalizacji statycznych może "ubić" Twój program w równie nieprzyjemny sposób, jak w przypadku zdefiniowanych przez Ciebie klas. Poniższy przykładowy kod pokazuje przyczynę tego problemu:

 #include <iostream>
 
 int f();  
// deklaracja
 int g();  
// deklaracja
 
 int x = f();
 int y = g();
 
 int f()
 {
   cout << "używam 'y' (które wynosi " << y << ")\n";
   return 3*y + 7;
 }
 
 int g()
 {
   cout << "inicjalizuję 'y'\n";
   return 5;
 }

Na podstawie komunikatów programu można zauważyć, że zmienna y zostałą użyta przed jej inicjalizacją. Rozwiązaniem, tak jak w poprzednim przypadku, jest metoda "konstruuj przy pierwszym użyciu" (ang. "Construct On First Use Idiom"):

 #include <iostream>
 
 int f();  
// deklaracja
 int g();  
// deklaracja
 
 int& x()
 {
   static int ans = f();
   return ans;
 }
 
 int& y()
 {
   static int ans = g();
   return ans;
 }
 
 int f()
 {
   cout << "używam 'y' (które wynosi " << y() << ")\n";
   return 3*y() + 7;
 }
 
 int g()
 {
   cout << "inicjalizuję 'y'\n";
   return 5;
 }

Oczywiście, całość można uprościć przenosząc kod inicjujący zmienne x i y do odpowiednich funkcji:

 #include <iostream>
 
 int& y();  
// deklaracja
 
 int& x()
 {
   static int ans;
 
   static bool firstTime = true;
   if (firstTime) {
     firstTime = false;
     cout << "używam 'y' (które wynosi " << y() << ")\n";
     ans = 3*y() + 7;
   }
 
   return ans;
 }
 
 int& y()
 {
   static int ans;
 
   static bool firstTime = true;
   if (firstTime) {
     firstTime = false;
     cout << "inicjalizuję 'y'\n";
     ans = 5;
   }
 
   return ans;
 }

I, jeśli nie masz nic przeciwko pozbyciu się poleceń wysyłających komunikaty, całość da się sprowadzić do naprawdę prostej postaci:

 int& y();  // deklaracja
 
 int& x()
 {
   static int ans = 3*y() + 7;
   return ans;
 }
 
 int& y()
 {
   static int ans = 5;
   return ans;
 }

Ponadto, skoro y jest inicjalizowane przy użyciu stałej wartości, funkcja y() przestaje już być potrzebna — można po prostu użyć zwykłej zmiennej.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.16] Co powinien zrobić konstruktor, któremu nie powiodło się utworzenie obiektu?

Wyrzucić wyjątek (zgłosić sytuację wyjątkową). Szczegóły w [17.2].

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[10.17] Co to są "nazwane parametry" (ang. "Named Parameter Idiom")?

Jest to całkiem użyteczne zastosowanie method chainingu.

Nazwane parametry rozwiązują pewien fundamentalny problem związany z językiem C++ - język ten obsługuje wyłącznie parametry pozycyjne. Np., przy wywołaniu jakiejkolwiek funkcji nie możemy napisać czegoś w rodzaju "Tu masz wartość dla parametru xyz, a to następne to wartość dla parametru pqr". Jedyne, co można w takiej sytuacji zrobić w C++ (jak również w C i Javie) jest napisanie czegoś w stylu "Tu masz pierwszy parametr, tu drugi, tam trzeci itd.". Alternatywa, w postaci nazwanych parametrów zaimplementowanych np. w języku Ada, jest szczególnie przydatna w przypadku funkcji, które pobierają dużą ilość parametrów, z których większość najczęściej przyjmuje te same, domyślne wartości, zaś zmieniane są tylko nieliczne.

Przez lata wymyślono wiele sposobów pozwalających obejść brak nazwanych parametrów w językach C i C++. Jeden z nich polega na przekazywaniu parametrów w postaci napisu, który jest interpretowany w czasie wykonywania programów. Jest to ta sama technika, jaką zastosowano w funkcji fopen() (drugi parametr, określający dostęp do pliku). Innym sposobem jest połączenie wszystkich boolowskich parametrów [tzn. mogących przyjmować tylko wartości true i false - przyp. tłum.] w jedną mapę bitową; w tym przypadku kod wywołujący taką funkcję musi z-OR-ować ze sobą całą grupę stałych zawierających odpowiednio poprzesuwane bity tak, aby otrzymać gotowy parametr. W ten sposób zrealizowano np. drugi parametr funkcji open. Wszystkie te rozwiązania są w pełni skuteczne, jednak technika "nazwanych parametrów" pozwala na uzyskanie wywołań, które są bardziej oczywiste, łatwiejsze do zapisania, łatwiejsze do przeczytania i, generalnie, bardziej eleganckie.

Pomysł polega na zastąpieniu parametrów funkcji metodami nowo utworzonej klasy, w której wszystkie te metody zwracają referencję do obiektu *this. Następnie z głównej funkcji usuwasz większość parametrów i zmieniasz ją w metodę wykonującą proste, ściśle określone działanie odpowiadające jej nazwie.

Dla lepszego zrozumienia poprzedniego akapitu zajmijmy się prostym przykładem.

Przykładem będzie funkcja służąca do otwierania pliku. Powiedzmy, że będzie ona wymagała parametru zawierającego nazwę pliku, jak i również posiadała opcjonalne parametry umożliwiające: ustawienie dostępu do pliku (tylko-odczyt lub odczyt-i-zapis lub tylko-zapis); określenie czy plik ma zostać utworzony w razie, gdyby nie istniał; czy wskaźnik zapisu powinien być umieszczony na końcu pliku (dołączanie - ang. "append") czy też na początku (nadpisywanie - ang. "overwrite"); podanie rozmiaru bloku, jeżeli plik musiałby zostać utworzony; określenie czy dostęp do pliku ma być buforowany czy też nie; podanie rozmiaru bufora; określenie, czy dostęp ma być współdzielony czy też ekskluzywny; i zapewne można by dodać jeszcze kilka innych opcji. Jeżeli powyższą koncepcję zaimplementowalibyśmy w postaci funkcji z parametrami pozycyjnymi, wywołanie takiej funkcji byłoby bardzo trudne do odczytania: mogłoby ono zawierać nawet ok. ośmiu parametrów i przy jego pisaniu programista prawdopodobnie często popełniałby błędy. Z tego powodu zastosujemy technikę "nazwanych parametrów".

Zanim zaczniemy pisać implementację, zobaczmy jak mogłoby wyglądać wywołanie naszej funkcji, zakłądając że większość parametrów ma zachować swoje domyślne wartości:

 File f = OpenFile("foo.txt");

To najprostszy przypadek. Zobaczmy teraz, jak mogłoby wyglądać wywołanie, jeśli chcielibyśmy zmienić garść parametrów:

 File f = OpenFile("foo.txt").
            readonly().
            createIfNotExist().
            appendWhenWriting().
            blockSize(1024).
            unbuffered().
            exclusiveAccess();

Zwróć uwagę, że "parametry", jeśli można je tak nazwać, mogą być zapisane w dowolnej kolejności (nie są one pozycyjne) i że wszystkie mają nazwy. Wobec tego programista nie musi pamiętać kolejności tych wszystkich parametrów, a ich nazwy są oczywiste (przynajmniej mam taką nadzieję).

Zatem czas już na implementację: najpierw tworzymy nową klasę (OpenFile), w której będą "mieszkać" wszystkie zmienne reprezentujące parametry (jako pola prywatne). Następnie wszystkie metody zmieniające te parametry (readonly(), blockSize(unsigned), etc.) muszą zwracać referencję do obiektu *this (tzn. referencję do obiektu OpenFile), dzięki czemu możliwe będzie wywoływanie tych metod w łańcuszku. Na końcu definiujemy wymagany parametr (w naszym przypadku nazwę pliku) jako zwykły, pozycyjny parametr konstruktora OpenFile.

 class File;
 
 class OpenFile {
 public:
   OpenFile(const string& filename);
     
// ustawia wszystkie parametry na domyślne wartości
   OpenFile& readonly();  
// ustawia readonly_ na true
   OpenFile& createIfNotExist();
   OpenFile& blockSize(unsigned nbytes);
   
// ...
 private:
   friend File;
   bool readonly_;       
// domyślnie false [na przykład]
   
// ...
   unsigned blockSize_;  
// domyślnie to 4096 [na przykład]
   
// ...
 };

Ostatnią rzeczą, jaką trzebaby zrobić, jest utworzenie konstruktora klasy File pobierającego jako parametr obiekt klasy OpenFile

 class File {
 public:
   File(const OpenFile& params);
     
// pobiera parametry z obiektu OpenFile i dokonuje "właściwego"
     
// otwarcia pliku.
 
   
// ...
 };

Zwróć uwagę, że klasa OpenFile deklaruje klasę File jako jej przyjaciela (słowo kluczowe friend), dzięki czemu OpenFile nie musi zawierać sterty publicznych metod służących do pobierania parametrów)) (które i tak nie byłyby przydatne nigdzie indziej poza konstruktorem File).

Ponieważ każda metoda-parametr w łańcuszku zwraca referencję, stąd obiekt OpenFile nie jest kopiowany, dzięki czemu łańcuszek jest dość wydajny. Ponadto, jeśli większość tych metod będzie inline, wygenerowany przez kompilator kod będzie prawdopodobnie porównywalny z analogicznym kodem w C ustawiającym parametry w polach struktury. Oczywiście, jeśli metody nie będą inline, może nastąpić nieznaczne zwiększenie rozmiaru kodu i pogorszenie jego wydajności (ale tylko jeśli konstrukcja następuje w krytycznym fragmencie programu, którego wydajność zależy głównie od szybkości procesora; wolałbym uniknąć otwierania tej Puszki Pandory; omówienie tej kwestii znajduje się w książce C++ FAQs), zatem w tym przypadku z "solidnością" kodu mogą się wiązać pewne koszty.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


E-Mail E-mail the author
C++ FAQ LiteSpis treściSkorowidzO autorze©Pobierz swoją własną kopię ]
Ostatnia aktualizacja Jun 17, 2002
Wersja polska: 0.1i Jul 12, 2004