Posts tagged ‘C/C++’

Unie i konstruktory

2007-09-18 19:17

Przychodzi czasem ochota, aby zapewnić dostęp do tych samych danych na różne sposoby. Ten pomysł można zrealizować w sposób poprawny albo i nie :) Dzisiaj właśnie przekonałem się ostatecznie, dlaczego stosowana przeze mnie metoda należała do tej drugiej kategorii.

Otóż dość powszechnie wykorzystuję pewną strukturę o nazwie SMouseEventArgs, która docelowego zawiera informacje o jakimś zdarzeniu związanym z myszą (klik, przesunięcie, itd.). Wśród tych informacji jest między innymi pozycja kursora w momencie zajścia zdarzenia, która początkowo była opisana po prostu przez dwa pola X i Y.

W pewnym momencie zauważyłem jednak, że dość często stosuję konstrukcję podobną do tej:

  1. SMouseEventArgs mea(/*...*/);
  2. // ...
  3. SomeMethod (POINT2F(mea.X, mea.Y));

gdzie POINT2F jest strukturą opisującą punkt na płaszczyźnie. Aby zaoszczędzić sobie pisania (i konstrukcji tymczasowego obiektu) postanowiłem w przypływie kreatywności dodać do SMouseEventArgs drugi sposób dostępu do tych współrzędnych – pole Position. Jak? Otóż… przy pomocy unii:

  1. struct SMouseEventArgs
  2. {
  3.    union
  4.    {
  5.       struct { float X, Y; };
  6.       POINT2F Position;
  7.    };
  8.  
  9.    // ...
  10. };

Sęk w tym, że POINT2F jest porządną strukturą i zawiera między innymi kilka konstruktorów. A jest niedozwolone, by obiekt klasy posiadającej zdefiniowany przez programistę konstruktor albo nietrywialny destruktor mógł być elementem unii.
Kompilator raczył mnie więc błędem, ale wówczas w kolejnym przypływie kreatywności stosowałem “objeście” w postaci anonimowej struktury:

  1. struct { POINT2F Position; };

I byłem święcie przekonany, że przecież skoro POINT2F nie alokuje własnej pamięci, nie otwiera plików ani nie robi żadnych innych tego typu czynności, po których trzeba by sprzątać, to przecież nic złego nie może się stać…

Naturalnie byłem w błędzie :) Dalszym elementem układanki jest konstruktor SMouseEventArgs, przyjmujący kilka parametrów i inicjalizujący nimi strukturę:

  1. SMouseEventArgs(float x, float, y /*... */) : X(x), Y(y) { }

Na oko niby wszystko jest w porządku. Tylko dlaczego cokolwiek byśmy przekazali jako x i y, w wynikowej strukturze zawsze i tak współrzędne będą zerami?!
Ot, kolejna nierozwiązania zagadka Wszechświata. Przynajmniej do chwili, gdy uświadomimy sobie dwie rzeczy:

  • konstruktor zawsze inicjalizuje wszystkie pola klasy – nie tylko te na liście inicjalizacyjnej
  • kolejność inicjalizacji tych pól jest taka sama jak kolejność ich deklaracji w klasie

Aplikując te zasady do powyższego przypadku, mamy bardzo ciekawy scenariusz. Mianowicie pola X i Y są poprawnie wypełniane parametrami konstruktora SMouseEventArgs, lecz w chwilę potem to samo miejsce pamięci jest… nadpisywane przez konstruktor POINT2F. Dlaczego? Ano dlatego, że pole Position też musi zostać zainicjowane, a domyślny konstruktor POINT2F wstępnie ustawia je na punkt (0,0).

Mniej więcej takie są skutki prób bycia mądrzejszym od twórców języka i kompilatora :) Można by oczywiście brnąć dalej w to rozwiązanie, zmieniając kolejność deklaracji pól albo jawnie inicjalizować pole Position zamiast X i Y. Cały czas jednak jest to stąpanie po cienkim lodzie.
Dlatego chyba najwyższy czas ograniczyć swoją kreatywność i następnym razem zastosować może mało efektowne, ale za to stuprocentowo bezpieczne metody dostępowe :)

Tags: , ,
Author: Xion, posted under Programming » 3 comments

Przeciążanie destruktorów

2007-09-15 11:41

Trochę poprzednio ponarzekałem na technikę RAII stosowaną w C++, a raczej na towarzyszący jej brak wygodnej instrukcji finally. Było to być może odrobinę niesprawiedliwe, gdyż mechanizm ma dość duże możliwości – przynajmniej potencjalnie :)

Natrafiłem jakiś czas temu na Usenecie na ciekawy pomysł związany z tą właśnie techniką. Chodzi tu o wykrywanie, z jakiego powodu obiekt lokalny ma zostać zniszczony. Może się to bowiem odbyć w normalny sposób (gdy kończy się wykonanie odpowiedniego bloku kodu i program przechodzi dalej) lub w wyniku odwijania stosu podczas obsługi wyjątku. W obu przypadkach w C++ jest jednak wywoływany jeden i ten sam destruktor.
Jest to w porządku, jeżeli jego zadaniem jest tylko zwolnienie zasobu (czyli np. zamknięcie otwartego deskryptora pliku). Możemy sobie aczkolwiek wyobrazić zastosowanie RAII do tzw. transakcji:

  1. try
  2. {
  3.    CTransaction ts;
  4.    ts.DoSomething();  // jakaś operacja
  5.  
  6.    // ...
  7.    if (SomethingBadHappened())
  8.       throw std::exception("Failure!");
  9. }
  10. catch (...) { /* ... */ }

Transakcja to termin znany głównie programistom zajmującym się bazami danych lub innymi pokrewnymi dziedzinami “sortowania ogórków” ;) W skrócie, jest to taki ciąg operacji, który musi być zaaplikowany albo w całości, albo wcale. Jeżeli po drodze zdarzy się coś nieoczekiwanego i transakcję trzeba przerwać, wszystkie wykonane do tej chwili operacji powinny zostać odwrócone (rollback).
Można by to zrobić automatycznie w reakcji na rzucenie wyjątku, gdyby C++ pozwalał na wykrycie wspomnianych dwóch sposobów niszczenia obiektu. Ciekawym pomysłem na to jest dopuszczenie więcej niż jednego destruktora:

  1. class CTransaction
  2. {
  3.    public:
  4.       CTransaction() { Begin(); }
  5.       ~CTransaction() { Commit(); }
  6.       ~CTransaction(const std::exception&) { Rollback(); }
  7. };

Zwykły, bezparametrowy, byłby wywoływany w przypadku zwyczajnego opuszczenia bloku kodu. Natomiast destruktor przyjmujący parametr włączałby się wówczas, gdy niszczenie obiektu zdarzy się z powodu wyjątku. Taki destruktor “łapałby” więc na chwilę taki wyjątek – lecz nie po to, by go obsłużyć, ale wyłącznie w celu odpowiedniego zakończenia życia obiektu w sytuacji kryzysowej. Parametr takiego destruktora odpowiadałby typowi wyjątku, który ten destruktor miałby “łapać”.

Obecnie nie jest naturalnie możliwe stosowanie w C++ takiej konstrukcji. Istnieje jednak sposób na sprawdzenie, czy jesteśmy właśnie w trakcie obsługi jakiegoś wyjątku. Służy do tego mało znana funkcja uncaught_exception z przestrzeni std (nagłówek exception):

  1. CTransaction::~CTransaction()
  2. {
  3.    if (std::uncaught_exception())
  4.       // destruktor wywołany przez wyjątek - odwracamy transakcję
  5.       Rollback();
  6.    else
  7.       // normalne wywołanie destruktora - zatwierdzamy
  8.       Commit();
  9. }

Wprawdzie nie zapewnia ona dostępu do samego obiektu wyjątku (ani poznania jego typu), ale pozwala na zorientowanie się, czy taki wyjątek w ogóle wystąpił. A to, jak widać, najczęściej wystarczy. Tak więc chociaż przeciążanie destruktorów na pierwszy rzut oka brzmi interesująco (i intrygująco), nie jest, jak sądzę, zbytnio potrzebne.

Tags: , , ,
Author: Xion, posted under Programming » 2 comments

Bolączki C++ #3 – RAII vs finally

2007-09-13 9:23

Wyjątki są sposobem na zasygnalizowanie nietypowych i niespodziewanych błędów, które poważnie zaburzają działanie programu. I właśnie to, że potencjalnie wyjątek może wystąpić w bardzo wielu miejscach w kodzie, rodzi pewne kłopoty. Problemami są chociażby zasoby: coś, co się pozyskuje, wykorzystuje, a następnie zwalnia, gdyż w przeciwnym razie doszłoby do wycieku. Typowym zasobem jest chociażby dynamicznie alokowana pamięć – jeżeli jej nie zwolnimy, nastąpi klasyczny wyciek, jako że C++ nie posiada garbage collectora, który mógłby się tym zająć za nas.

W C++ zalecanym rozwiązaniem tego problemu jest technika znana jako RAII (Resource Acquision Is Initialization – pozyskanie zasobu jest inicjalizacją). Korzysta ona z faktu, że w naszym ulubionym języku programowania możemy tworzyć obiekty lokalne z konstruktorami i destruktorami. Te drugie wywołają się zawsze przy opuszczaniu danego bloku kodu – niezależnie od tego, czy stało się z powodu wyjątku czy tez normalnego przebiegu programu. Pomysł polega więc na tym, by tworzyć obiekt specjalnie przygotowanej klasy w momencie pozyskania zasobu, zaś destruktor tego obiektu zajmie się już jego zwolnieniem, niezależnie od powodu.

  1. try
  2. {
  3.    // 'wskaźnik lokalny' - chroni przed wyciekiem pamięci
  4.    std::auto_ptr<CFoo> pFoo(new CFoo(...));i
  5.  
  6.    // strumień plikowy - automatycznie zamyka otwarty plik
  7.    std::fstream FS("file.txt", std::ios::out);
  8. }
  9. catch (...) { /* ... */ }

Dopóki korzystamy z pamięci albo z plików, wszystko jest w porządku; odpowiednie klasy (jak auto_ptr) posiada bowiem Biblioteka Standardowa. Gorzej jeśli chcemy skorzystać z innego rodzaju zasobów. Jeśli odpowiednia klasa realizująca technikę RAII nie istnieje, nie pozostaje nam nic innego, jak samemu ją sobie zapewnić (czytaj: napisać). I tak dla każdego rodzaju niestandardowych zasobów, które używamy. Po niedługim czasie można by z tych klas ułożyć własną “bibliotekę standardową” ;)

Alternatywą dla RAII jest dodanie trzeciego bloku (po try i catch) do konstrukcji łapiącej wyjątki. Jest on zwykle nazywany finally. Instrukcje zawarte w tym bloku są wykonywane zawsze po tych z bloku try – niezależnie od tego czy wyjątek wystąpił czy nie. Jest to więc bardzo dobre miejsce na wszelki kod zwalniający pozyskane wcześniej zasoby, np.:

  1. import java.io.*;
  2.  
  3. try
  4. {
  5.    FileReader fr = new FileReader("file.txt");
  6.  
  7.    // (czytanie pliku)
  8. }
  9. finally { fr.close(); }

Co ciekawe, posiadają go języki, które jeden z najważniejszych zasobów – pamięć – mają zarządzaną przez odśmiecacz, który praktycznie wyklucza możliwość powstania wycieków. Rzecz jednak w tym, że niektóre zasoby, jak chociażby otwarte pliku, nie mogą sobie czekać na to, aż odśmiecacz przypomni sobie o nich, gdyż wtedy byłyby blokowane stanowczo zbyt długo.

Czy C++ też potrzebuje instrukcji finally? Na pewno nie jest to bardzo paląca potrzeba, jako że technika RAII zapewnia komplet potrzebnej tutaj funkcjonalności. To drugie, alternatywne rozwiązanie ma jednak szereg zalet:

  • Brak konieczności opakowywania każdego wykorzystywanego zasobu w specjalną klasę.
  • Większa przejrzystość kodu, w którym zarówno operację pozyskania, jak i zwolnienia zasobu.
  • finally można też pożytecznie wykorzystać nawet wówczas, gdy w grę nie wchodzi możliwość pojawienia się wyjątku. Jeżeli na przykład w jakiejś skomplikowanej funkcji mamy wiele miejsc, w których może nastąpić jej zakończenie, a przy każdej okazji może być potrzeba wykonania jeszcze jakichś czynności końcowych. Wtedy moglibyśmy zamknąć całą treść funkcji w try, a owe czynności umieścić w sekcji finally. Trzeba by się było jednak liczyć z tym, że blok try nie jest darmowy i jego użyciu nakłada pewien narzut.

I przede wszystkim: RAII i finally nie wykluczają się nawzajem. Dlatego obecność tego drugiego mechanizmu w C++ na pewno by nam nie zaszkodziła :)

Tags: , ,
Author: Xion, posted under Programming » Comments Off on Bolączki C++ #3 – RAII vs finally

Domykanie klas i metod

2007-09-09 12:10

W teorii dziedziczenie to niezbyt skomplikowana relacja między klasami, ale w praktyce potrzeba czasem niestandardowych rozwiązań. Niekiedy bowiem możliwość rozszerzenia hierarchii klas w nieskończoność nie jest wcale pożądana. Zazwyczaj chodzi tu o względy projektowe, rzadziej o efektywność.
Pytanie brzmi: czy można jakoś zabezpieczyć konkretną klasę przed dziedziczeniem (czyli zabronić w tworzenia opartych o nią klas pochodnych)?

W C# i Javie jest to możliwe za pomocą specjalnych słów kluczowych – odpowiednio sealed (‘zapieczętowany’) i final. Wystarczy opatrzyć nimi definicję klasy, by kompilator odmówił skompilowania kodu, który wykorzystywałby taką klasę jako bazową:

  1. public sealed class Foo { };
  2.  
  3. // błąd - dziedziczenie zapieczętowanej klasy
  4. public class Bar : Foo { };

Modyfikatory sealed/final można też stosować do metod wirtualnych. W tym przypadku sprawiają one, że dana metoda wirtualna nie może być nadpisywana w klasach pochodnych. Chwilowo nie przychodzi mi do głowy żaden pomysł na pokazanie przydatności takiego triku, ale zapewne takowy przykład istnieje :)

W C++ nie mamy rzecz jasna wspomnianych słów kluczowych (ani żadnych, które działałyby podobnie), ale istnieje sposób na zabezpieczenie klasy przed dziedziczeniem. Jest on jednak brzydki. A nawet bardzo brzydki. Mimo to zaprezentuję go.
Jak to często bywa w takich sytuacjach, sztuczka polega na kreatywnym wykorzystaniu pewnego mechanizmu językowego unikalnego dla C++, który generalnie służy do czegoś zupełnie innego. Tutaj chodzi o dziedziczenie wirtualne – a dokładniej o to, że wirtualna klasa bazowa musi być bezpośrednio inicjalizowana przez każdą klasę pochodną; nawet tą w setnym pokoleniu niżej. Do tego dodać należy jeszcze prywatne konstruktory i deklarację przyjaźni, a następnie zamieszać i doprawić do smaku, by ostatecznie upichcić coś w tym stylu:

  1. class CFoo; // deklaracja klasy, którą chcemy zabezpieczyć przed dziedziczeniem
  2.  
  3. // sztuczna klasa pomocnicza - "blokada"
  4. class CFoo_Lock
  5. {
  6.    // deklaracja przyjaźni
  7.    friend class CFoo;
  8.  
  9.    private:
  10.       // prywatne konstruktory
  11.       CFoo_Lock() { }
  12.       CFoo_Lock(const CFoo_Lock&) { }
  13. };
  14.  
  15. // zapieczętowana klasa
  16. class CFoo : public virtual CFoo_Lock { /* ... */ };
  17.  
  18. // próba dziedziczenia - błąd: brak dostępu do konstruktora CFoo_Lock
  19. class CBar : public CFoo { /* ... */ };

Klasa pomocnicza CFoo_Lock ma prywatny konstruktor domyślny, więc generalnie nie może być on wywoływany. W szczególności nie może być on wywołany przez konstruktor CBar – a jest to konieczne, gdyż CFoo_Lock jest wirtualną klasą bazową i musi być zainicjalizowana bezpośrednio przez dowolną klasę pochodną (nawet niebezpośrednio pochodną). Dlatego też próba dziedziczenia CFoo skończy się błędem mówiącym o braku dostępu do prywatnego konstruktora CFoo_Lock.
Dlaczego zatem CFoo działa? To już zasługa deklaracji przyjaźni umieszczonej w klasie-blokadzie. Przyjaźń międzyklasowa nie jest jednak dziedziczna, więc nasza blokada odmawia dostępu do prywatnych składowych (czyli konstruktorów) klasom pochodnym CFoo. Dzięki temu (i niuansom dziedziczenia wirtualnego) cały ten trik działa.

To rozwiązanie można naturalnie uczynić bardziej automatycznym – wystarczy zaprząc do pracy szablony i preprocesor, by uzyskać prawie to samo co sealed/final w C#/Javie. Trudno będzie jednak uznać to kombinowanie za eleganckie.
Wygląda więc na to, że najlepszym sposobem na zabezpieczenie klasy przed dziedziczeniem w C++, to po prostu… pokazać na nią palcem (tj. komentarzem) i powiedzieć “Nie dziedzicz mnie!”. No i mieć nadzieję, że ktoś nas posłucha.

Tags: , , ,
Author: Xion, posted under Programming » 1 comment

Bolączki C++ #2 – Pakietowanie kodu

2007-09-01 19:05

W czasach panowania języków proceduralnych i strukturalnych – jak C czy Pascal – możliwości ukrywania i ochrony kodu przed niepowołaną zmianą były, delikatnie mówiąc, słabe. Wszystkie funkcje, pola i struktury były dostępne na zewnątrz i tylko dzięki programistycznej kurtuazji (i czytaniu dokumentacji technicznej, o ile istniała) zawdzięczało się poprawne działanie kodu.
Sytuacja poprawiła się wraz z językami obiektowymi, takimi jak C++. Mamy już klasy, których składniki mogą być określone jako publiczne, chronione (protected) albo prywatne. Jeżeli tylko odpowiednio oznaczymy składowe należące do implementacji, nikt niepowołany nie będzie miał do nich dostępu zwyczajnymi środkami.
Od tego czasu świat poszedł jednak do przodu pod względem technik organizacji kodu. Nowsze języki programowania – jak Java, C# czy Python – posiadają wbudowane mechanizmy pakietów (zwanych w .NET złożeniami – assemblies). Dzięki nim można grupować klasy wykonujące wspólne zadania i zapewniać dostęp do wybranych składowych wszystkich klasom z danego pakietu. W precyzyjnym określaniu widoczności celuje zwłaszcza C#.

W C++ teoretycznie mamy dość podobne możliwości, oferowane przez kilka osobnych mechanizmów językowych. Czymś co zdaje się najbardziej przypominać pakiety lub złożenia są przestrzenie nazw (namespaces). Ich przeznaczeniem jest przede wszystkich jednak zabezpieczenie przed dublowaniem i niejednoznacznością nazw. Na przykład klasa string z STL jest zawarta w przestrzeni std i dzięki temu nie będzie pomylona z jakąś inną klasą łańcuchów znakowych o tej samej nazwie.
Jednak przestrzenie nazw to bardzo ułomny sposób pakietowania. Elementy jednej przestrzeni (klasy, funkcje) domyślnie są dla siebie tak samo obce, jak te należące do różnych przestrzeni. Można to oczywiście zmienić, korzystając z deklaracji przyjaźni; wymaga to jednak znajomości wszystkich tych “przyjaciół” – czyli odpowiednich deklaracji zapowiadających – co jest bardzo niewygodne. Naturalnie można powiedzieć, że wtedy przynajmniej wiemy, kto nas “podgląda”. Lecz co z tego, skoro w takim zaprzyjaźnianiu stosować można wyłącznie zasadę wszystko albo nic: klasa/funkcja będzie widziała albo wszystko składowe, albo tylko publiczne (domyślnie). Bez pokrętnych sztuczek nie da się udostępnić pewnych składników tylko w ramach “pakietu”.

Całkiem osobną kwestią jest też to, jak mechanizm przestrzeni nazw współpracuje z tą zakałą C++, czyli plikami nagłówkowymi i dyrektywą #include. W nowszym językach wystarcza instrukcja podobnej do tej:

  1. using System.Windows.Forms;

żeby jednocześnie zadeklarować użycie pakietu i uchronić się przed koniecznością używania kwalifikowanych nazw (w tym przypadku np. ListBox zamiast System.Windows.Forms.ListBox). W C++ potrzebujemy do tego dwóch dyrektyw:

  1. #include <vector>
  2. using namespace std;

i niby wszystko byłoby w porządku. Problem w tym, że wielce inteligentny mechanizm plików nagłówkowych oraz #include sprawia, że nazw niekwalifikowanych możemy używać praktycznie tylko w plikach *.cpp. Gdyby bowiem umieścić using namespace w nagłówku, to instrukcja ta zostałaby bezmyślnie powielona we wszystkich plikach, które ten nagłówek dołączają. A wtedy przestrzeń nazw nie spełniałaby nawet tej najprymitywniejszej funkcji, czyli ochrony przed wieloznacznością powtarzających się nazw.

Tak naprawdę to same przestrzenie nazw nie są złe. Nawet same pliki nagłówkowe z C++ nie byłyby aż tak bardzo złe, gdyby ich zadaniem nie było tylko ułatwianie życia kompilatorowi. Dopiero połączenie tych dwóch rzeczy sprawia, że zdecydowanie odechciewa się używać którejkolwiek z nich ;P
Istnieje jednak pewien optymistyczny akcent. Jeśli kiedykolwiek zostanie zniesiony w C++ podział na dwa rodzaje plików z kodem, wówczas i przestrzenie nazw będą musiały się przekształcić w coś podobnego do pakietów Javy czy złożeń z .NET. Podobnie jest w drugą stronę: jeżeli C++ dorobi się mechanizmu pakietów, wtedy pliki nagłówkowe i dyrektywa #include nie będą miały wielkiego sensu. Wypada więc tylko kibicować Komitetowi Standaryzacyjnemu w wykonaniu tego pierwszego kroku, bo drugi prawdopodobnie zrobią siłą rozpędu :)

Tags: ,
Author: Xion, posted under Programming » 3 comments

Bolączki C++ #1 – Pimp(l) my code

2007-08-25 12:13

Kiedy programuje się w języku, który jest – jak to ładnie mówią Amerykanie – ‘standardem przemysłowym’ w danej dziedzinie, chcąc nie chcąc trzeba się do niego przyzwyczaić. A to oznacza, że musimy nauczyć się żyć z jego wadami, które niekiedy mogą być tylko irytujące, a niekiedy bardzo nieprzyjemne. Ważne, by mieć świadomość ich istnienia i w miarę możliwości sobie z nimi radzić.
C++ jako ustandaryzowany język ma już prawie dziesięć lat, więc lista jego niedoskonałości w porównaniu z nowszymi językami siłą rzeczy staje się coraz dłuższa. Takie listy są jednak w dużym stopniu subiektywne, zatem i ten, który rozpoczynam poniżej, absolutnie nie pretenduje do miana ostatecznej wyroczni :) Na pewno inni usunęliby z niego część pozycji, niektóre uznali za mniej lub bardziej dotkliwe, a także dodali własne propozycje.

Dla mnie sprawą, która w C++ jest powodem największego bólu zębów, jest sposób organizacji kodu zaproponowany w tym języku. Jest on bodaj jedynym mi znanym (poza swoim poprzednikiem C), w którym występuje podział plików z kodem na dwa rodzaje: pliki nagłówkowe (*.h, *.hpp) i moduły kodu (*.cpp). W tych pierwszych teoretycznie umieszczany jest interfejs klas i funkcji, czyli informacje potrzebne do ich użycia w innym miejscu programu. W tych drugich jest zaś zawarta implementacja rzeczonych klas oraz funkcji.
Tyle teorii. W praktyce osiągnięcie idealnej separacji obu tych elementów wymaga sztuczki nazywanej szumnie wzorcem projektowym, o nazwie Pimpl (skrót od private implementation). Wygląda on na przykład następująco:

  1. // -- foo.hpp --
  2. class CFoo_Impl;   // deklaracja zapowiadająca
  3.  
  4. // właściwa klasa
  5. class CFoo
  6. {
  7.     private:
  8.         CFoo_Impl*  m_pImpl;
  9.  
  10.     public:
  11.         CFoo();
  12.         ~CFoo();
  13.         void SomeOperation1();
  14. };
  15.  
  16. // -- foo.cpp --
  17. #include "foo.hpp"
  18.  
  19. class CFoo_Impl
  20. {
  21.      // implementacja
  22. };
  23.  
  24. CFoo::CFoo() : m_pImpl(new CFoo_Impl) { /* ... */}
  25. CFoo::~CFoo() { delete m_pImpl; }
  26. void CFoo::SomeOperation1() { m_pImpl->SomeOperation1(); }
  27. // itd.

Wtedy faktycznie nie widać, co siedzi w środku naszej klasy, ale cena takiej hermetyzacji to konieczność stworzenia dodatkowej klasy i przekierowania do niej metod. Może nie jest to dwa razy więcej roboty, ale przynajmniej 10-20%.
Z drugiej strony większość języków nowszych niż C++, jak Java, C# czy Python, w ogóle zna takich pojęć jak ‘prototyp funkcji’ czy ‘deklaracja zapowiadająca’. Tam kod piszemy od początku do końca: funkcję zawsze z jej treścią, a klasę zawsze w całości łącznie ze wszystkimi polami i kodem wszystkich metod. Nie ma podziału na pliki z ‘interfejsem’ i z implementacją.
A w C++ ten podział istnieje i tak naprawdę służy on tylko i wyłącznie… wygodzie kompilatora. Dzięki temu, że treść plików nagłówkowych jest taka, a nie inna (i np. zawierają one deklaracje prywatnych pól klas, które są przecież częścią implementacji), mogą być one zwyczajnie dołączane do modułów przy pomocy arcyprymitywnej dyrektywy #include. Dla kompilatora jest to najprostsze rozwiązanie, bo może on pracować nad każdym modułem osobno i nie musi się martwić żadnymi niejawnymi zależnościami między plikami. A dla programisty oznacza to wybór między wygodą kodowania, a jakością i “odpornością” stworzonego kodu.

C++ stoi więc w pewnym sensie pośrodku i wcale nie jest to złoty środek. Z jednej strony mógłby pójść w kierunku wyznaczonym przez wspomniane nowsze języki, czyli wprowadzenia jednego typu plików źródłowych, zawierających kod bez podziału na deklaracje i implementacje. To by było jednak mało oryginalne :) Bardziej interesujące byłoby, jak sądzę, polepszenie istniejącego rozwiązania w odwrotnym kierunku; być może automatyczne stosowanie wzorca Pimpl dla każdej klasy jest jednym ze sposobów.
Niestety, patrząc na obecny roboczy szkic standardu C++0x, w którym nic na ten temat nie znajdziemy, nie możemy raczej oczekiwać, że coś tu się zmieni w przewidywalnej przyszłości.

Tags: , ,
Author: Xion, posted under Programming » 3 comments

Liczenie stałych

2007-08-07 11:59

Dzisiaj pokażę pewną sztuczkę, mogącą nieco ułatwić życie programiście, który – jak wiadomo – zawsze ma za dużo pracy. Nie jest ona zbyt odkrywcza ani pomysłowa, ale ponieważ wielokrotnie zdarzyło mi się z niej korzystać (ostatni raz całkiem niedawno), sądzę że zasługuje na wzmiankę.

Sprawa dotyczy typów wyliczeniowych w C++ – czyli tworów, które pojawiają się często w większości programów. W moim ostatnim przypadku był to prosty typ definiujący wachlarz kolorów używanych w różnych miejscach systemu GUI:

  1. enum GUI_COLOR
  2. {
  3.     // kolory pasków tytułu okien
  4.     GC_ACTIVE_CAPTION, GC_INACTIVE_CAPTION,
  5.     // kolory kontrolek
  6.     GC_CONTROL_NORMAL, GC_CONTROL_HOVER,
  7.     // kolor tła okna
  8.     GC_WINDOW
  9. };

Jest to podobny zestaw do tego, jaki można zobaczyć w Windows w oknie Właściwości: Ekran, na zakładce Wygląd. Naturalnie jest on dość okrojony, jako że nie potrzebujemy tutaj niczego aż tak skomplikowanego. Nie jest jednak wykluczone, że kiedyś się rozrośnie…
Każdemu z tych “systemowych” kolorów trzeba teraz przyporządkować kolor rzeczywisty – żeby wiedzieć, jak narysować konkretne kontrolki. Można zdefiniować do tego sporo zmiennych w stylu clActiveCaptionColor czy clControlNormalColor, ale lepszym rozwiązaniem jest tablica:

  1. COLOR aColors[???];

Problem w tym, że trzeba podać jej rozmiar. Można oczywiście wpisać tam na sztywno 5, ale doskonale wiadomo, jakie są skutki stosowania w kodzie “magicznych liczb”. Można zdefiniować sobie stałą, lecz wtedy też będziemy musieli ręcznie modyfikować jej wartość, jeżeli liczba kolorów się zmieni.

Potrzebujemy więc sposobu na automatyczne określenie liczby stałych zdefiniowanych w typie wyliczeniowym. W językach dysponujących dynamicznymi informacjami o typie – jak C# czy Java – byłoby to zapewne proste, lecz tutaj nie mamy tego komfortu. Zamiast tego możemy sobie jednak poradzić inaczej – dodając do typu wyliczeniowego kolejną stałą:

  1. enum GUI_COLOR
  2. {
  3.    /* ... */
  4.  
  5.    // na końcu
  6.    GC_COUNT
  7. };

Kompilator nada jej wartość o jeden większą od poprzedniej. Ponieważ pierwszej stałej przypisał zero, GC_COUNT będzie odpowiadało liczbie 5 i to będzie właśnie liczba użytecznych stałych w typie wyliczeniowym. Teraz można już zadeklarować potrzebną tablicę:

  1. COLOR aColors[GC_COUNT];

Podobnie można zrobić dla każdego typu wyliczeniowego. Najlepiej działa to wtedy, gdy kompilator sam numeruje jego stałe. Jeżeli sami to robimy, to oczywiście trik nadal będzie działał (zmienna *_COUNT będzie zawsze miała największa wartość). W tablicy pojawią się jednak niewykorzystane elementy i te dziury ewentualnie trzeba będzie omijać.

Tags: , ,
Author: Xion, posted under Programming » Comments Off on Liczenie stałych
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.