Archive for Programming

Przypisania złożone

2008-09-01 13:58

Przy wprowadzaniu operatorów przypisania typu += czy *= w większości kursów C++ stwierdza się, że ich używanie jest głównie skrótem w stosunku do korzystania ze zwykłego przypisania (=) i odpowiednich operatorów binarnych (jak + czy *). Takie wyjaśnienie jest na początek zupełnie wystarczające, a przy tym łatwe do zrozumienia i co ważniejsze, wydaje się poprawne. I tak faktycznie jest – ono tylko “wydaje się” :)
Później dowiadujemy się bowiem, że sprawa jest nieco bardziej skomplikowana. Zapis typu a += b nie musi zawsze nawet w przybliżeniu odpowiadać “wersji długiej” w postaci a = a + b. Trzeba tu wziąć pod uwagę kilka rzeczy – głównie to, co kryje się pod nazwami a i b:

  • Jeśli na przykład mamy do czynienia z przeciążonymi operatorami = i np. +, to nie oznacza to, że automatycznie dostajemy też przeciążony operator += (i vice versa). Po “skróceniu” dana instrukcja może się więc w ogóle nie skompilować. Dotyczy to oczywiście sytuacji, w których mamy do czynienia z własnymi typami danych.
  • W przypadku, gdy prawa strona przypisania jest bardziej rozbudowana, nieopatrzna zmiana operatora może spowodować niezamierzone błędy. W szczególności np. x = x - y + 2; to nie jest to samo co x -= y + 2;.
  • Nawet dla typów podstawowych skompilowany kod będzie najprawdopodobniej inny w zależności od tego, którego wariantu użyjemy – zwłaszcza w bardziej skomplikowanych przypadkach, gdy ewentualne optymalizacje nie są oczywiste.

Pamiętajmy więc, że operatory typu += są właśnie operatorami, całkowicie odrębnymi od wszystkich innych, nie zaś żadnymi skrótami, jak to się często “dla uproszczenia” mówi.

Tags:
Author: Xion, posted under Programming » Comments Off on Przypisania złożone

Metody wirtualne i override

2008-08-29 15:43

W wielu językach obiektowych występuje słowo kluczowe override, którego dodawanie jest obowiązkowe podczas nadpisywania metod wirtualnych w klasach pochodnych (w przeciwnym wypadku generowanie jest ostrzeżenie). Jak można się spodziewać, C++ do takich języków nie należy :) A szkoda, bo wymóg stosowania override zapobiega pomyłkom przy nadpisywaniu (np. literówkom w nazwach metod), które można wykryć dopiero w trakcie działania programu, gdy ze zdziwieniem odkrywamy, że zamiast nowej wersji metody wywoływana jest stara.

Można częściowo temu zaradzić, ale sposób jest brzydki. Polega on na stworzeniu makra zawierającego całą definicję klasy (albo przynajmniej deklaracje jej metod wirtualnych):

  1. #define DECLARE_FOO(__suffix__) \
  2.     public: \
  3.         virtual void AbstractDoSomething() __suffix__; \
  4.         virtual void DoSomethingElse();

Następnie używamy go zarówno w definicji klasy bazowej, jak i pochodnych:

  1. class Foo
  2. {
  3.     DECLARE_FOO(=0)
  4. };
  5.  
  6. class Bar : public Foo
  7. {
  8.     DECLARE_FOO( )
  9. };

Dodatkowy parametr pozwala odróżnić obie sytuacje, co jest koniecznie w przypadku metod czysto wirtualnych (które nie posiadają implementacji).

Sztuczka jest być może pomysłowa, ale jednocześnie dość przygnębiająca. Dlaczego bowiem tak prosta sprawa jak zapobieganie pomyłkom przy nadpisywaniu metod wirtualnych musi być w C++ realizowana za pomocą brzydkich makr?…

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

Pospolite ruszenie

2008-08-27 8:20

Okazjonalnie na Warsztacie zdarza się, że ktoś zaproponuje zebranie całego community i stworzenie czegoś wspólnymi siłami. To ‘coś’ oznacza oczywiście jakąś grę. Ponieważ zwykle robią to nowi członkowie społeczności, niemal jedynym skutkiem podobnej propozycji jest ożywiona dyskusja na temat tego, dlaczego to się na pewno nie uda ;] Zresztą, taka opinia jak dotąd okazywała się jak najbardziej słuszna :D

Tym razem jednak – choć zaczęło się podobno – w sprawę niespodziewanie zaangażowały się osoby cieszące się dużym autorytetem na Warsztacie. Do pracy nad projektem zgłosiło się też całkiem sporo osób, co aczkolwiek nie musi automatycznie wróżyć sukcesu, ale świadczy przynajmniej o dużym zapale początkowym (oby nie okazał się słomiany ;]). Wreszcie, nie było zbyt dużo zwyczajowego gadania i narzekania, bo prawie od razu rzecz przeszła do czynów – między innymi do utworzenia środowiska do pracy grupowej w serwisie Assembla.

A zatem – kto wie? Możliwe, że coś z tego wyjdzie… Ale przynajmniej na razie postaram się jeszcze zbytnio nie zapeszać :)

Tags:
Author: Xion, posted under Games, Programming » 5 comments

Używanie nagłówków Windows

2008-08-22 13:21

Chociaż Windows API ciągnie za sobą skutki kilkunastu lat wstecznej kompatybilności, nie znaczy to, że nie pojawiają się tam nowe możliwości. Ponieważ jednak wszystkie funkcje są importowane z DLL-i w sposób statyczny, nie jest możliwe, by każda nowsza była dostępna automatycznie. W przeciwnym wypadku program, który faktycznie działałby bez problemu np. na Windows 98 wymagałby w praktyce najnowszej wersji systemu – mimo tego, że nie korzysta z żadnej nowej funkcjonalności.
Niekiedy jednak nie chcemy ograniczać się do “wspólnego mianownika” wszystkich aktualnie obsługiwanych wersji Windows i chcemy użyć czegoś, co rzeczywiście jest dostępne począwszy od jakiejś nowszej edycji systemu. Co wtedy zrobić?

W dokumentacji (czyli MSDN) jest na szczęście dokładnie zaznaczone to, w której wersji Windows wprowadzoną daną funkcję czy strukturę. Ale żeby odpowiedni symbol został włączony do kompilacji i linkowania, należy jeszcze przed dołączeniem windowsowego nagłówka (zazwyczaj windows.h) zdefiniować odpowiednie makro.
A właściwie to nawet więcej niż jedno, jako że MSDN wspomina o czterech. Są to:

  • WINVER i _WIN32_WINNT, wskazujące na liczbową wersję systemu. Wartość 0x0502 oznacza na przykład wersję 5.1, czyli Windows XP z SP2.
  • _WIN32_IE, mające zawierać docelową wymaganą wersję Internet Explorera
  • NTDDI_VERSION, ustawialne na którąś z predefiniowanych stałych przypisanych każdej większej edycji Windows (np. NTDDI_WINXPSP1 lub NTDDI_WIN2K)

Pierwsze trzy są już uważane za przestarzałe i aktualnie używać należy NTDDI_VERSION, które jest zresztą znacznie wygodniejsze. W praktyce dobrze jest jednak ustawiać wszystkie makra, zwłaszcza jeśli nasz kod może być potencjalnie kompilowany na wielu różnych wersjach Platform SDK. I tak na przykład jeśli chcemy wykorzystać pule wątków (thread pools) dostępne począwszy od Windows Vista, powinniśmy dodać taki oto kod:

  1. #define NTDDI_VERSION NTDDI_VISTA
  2. #define WINVER 0x0600
  3. #define _WIN32_WINNT 0x0600
  4. #include <windows.h>

Po wartości makr odpowiadające wszystkim edycjom Windows należy udać do MSDN, do sekcji:

Win32 and COM Development > Development Guides > Windows API > Using Windows Headers

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

To, czego oczekuje kompilator

2008-08-19 12:53

Język C++ jest tworem skomplikowanym i jego złożoność daje się we znaki nie tylko programistom. Dość często mają z nią problemy także kompilatory. Obecnie zdecydowana większość narzędzi tego typu spełnia prawie całkowicie wymagania aktualnego standardu. Istnieje jednak sporo miejsca na usprawnienia, chociażby w bodaj najbardziej niedopracowanej dziedzinie: jakości komunikatów o błędach generowanych przez kompilatory.
Jeśli na przykład trafi nam się taka deklaracja zmiennej:

  1. SomeType variable;

a typ SomeType z jakiegoś powodu (niedołączony nagłówek, kwestia zasięgu, itd.) będzie w jej miejscu nieznany, to treść wyprodukowanego przy okazji komunikatu o błędzie może nas trochę zaskoczyć. Zarówno CL (kompilator Microsoftu używany w Visual C++), jak i GCC wyplują bowiem coś w stylu:

error: expected ‘;’ before ‘variable’

To znaczy ni mniej, ni więcej, tylko to, że według tych kompilatorów właściwa jest “deklaracja”:

  1. SomeType;

Dość zaskakujące, nieprawdaż?

Takie zachowanie można częściowo wyjaśnić tym, że kompilator “nie patrzy” na kod tak, jak programista. My widzimy tutaj deklarację zmiennej, natomiast dla kompilatora jest to tylko sekwencja: ‘nieznany identyfikator’, po którym następuje kolejny ‘nieznany identyfikator’. Ponieważ mówi się, że C++ jest językiem kontekstowym (co swoją drogą nie jest do końca ścisłe), taki fragment może sugerować wiele różnych rodzajów pomyłek. To, co sugeruje kompilator, wbrew pozorom też pewnie ma sens, chociaż dość specyficzny. Postawienie średnika w “spodziewanym” miejscu sprawi bowiem, że zamiast dwóch nieznanych nazw w jednej instrukcji będziemy mieli tylko jedną. Cóż za postęp! :)
Na tym prostym przykładzie widać, że interpretacja wyników kompilacji zakończonej niepowodzeniem wymaga niestety pewnego doświadczenia. Dopiero po jakimś czasie nauczymy się trafnie(j) zgadywać, co tak naprawdę oznacza ten czy inny błąd. Najwyraźniej taki już urok C++…

Tags:
Author: Xion, posted under Applications, Programming » 12 comments

Makra z wielokropkiem

2008-08-14 8:24

Chociaż nie jest zalecane, w C++ (podobnie jak w jego poprzedniku, C) możliwe jest wciąż tworzenie funkcji z nieokreśloną liczbą argumentów. Najbardziej znanym przykładem jest oczywiście printf:

  1. void printf(const char*, ...);

Nie wszyscy wiedzą jednak, że zmienną liczbą parametrów mogą mieć też makra (co nazywa się po angielsku variadic macros). Deklaruje się je wtedy w bardzo podobny sposób jak funkcje, też używając wielokropka:

  1. #define LOG(fmt, ...) fprintf(Logger::GetFile(), fmt, __VA_ARGS__);

Żeby jednak dostać się do ‘listy argumentów’ makra, używamy specjalnego identyfikatora __VA_ARGS__. W jego miejsce zostaną podstawione wszystkie podane parametry (z wyjątkiem pierwszego), oddzielone przecinkami tak, jak w oryginalnym “wywołaniu” makra.

Do czego może się to przydać? Jak widać powyżej, za pomocą takich makr możemy na przykład opakowywać wywołania funkcji ze zmienną liczbą argumentów – jak w powyższym przykładzie z logowaniem. Prawdopodobnie mogą być one przydatne także w implementacji jakiejś formy delegatów w C++, zwłaszcza jeśli chodzi o sposób ich wywoływania. Istnieje też ciekawa technika, pozwalająca na tworzenie (w C99 i późniejszych wersjach C/C++) funkcji z nieokreśloną liczbą argumentów, ale z zachowaną kontrolą ich typów.

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

Długie ciągi odwołań

2008-08-11 22:12

Chociaż obiekty powinny być jak najbardziej autonomiczne i możliwie niezależne od innych, to bardzo często zdarza się sytuacja, gdy trzeba “zrobić coś” używając czegoś “powszechnie dostępnego”. W praktyce chodzi też o to, by obiekty nie miały kilometrowej długości konstruktorów, do których trzeba by przekazywać wszystko, z czego ewentualnie będą one chciały kiedyś skorzystać.
Dlatego też stosuje się raczej inne rozwiązania, pozwalające w razie potrzeby ‘skoczyć’ w inne miejsce całej sieci powiązań między obiektami, jaka zwykle występuje w programie.

Sieć ta ma zresztą często formę drzewka, wychodzącego od wspólnego korzenia (jakiegoś ‘superobiektu’, reprezentującego całą aplikację), zawierającego w sobie wszystkie obiekty niższych rzędów. Logiczne jest więc zapewnienie odpowiednich metod, pozwalających na dostęp do nich. Wówczas można zawsze rozpocząć od samej góry i, schodząc coraz niżej, dojść w końcu do interesującego nas obiektu.
Z drugiej strony można też rozpoczynać wędrówkę zawsze od obiektu, w którym aktualnie jesteśmy, i w razie potrzeby poruszać się też w górę. Aby było to możliwe, należy jednak dla każdego obiektu jasno określić jego obiekt nadrzędny (parent) i dać możliwość dostępu do niego (np. poprzez przekazanie w konstruktorze odpowiedniego wskaźnika).

Niestety, obie te metody mogą w niektórych sytuacjach zaowocować długimi ciągami wywołań w rodzaju:

  1. ParentObject()->ParentObject()->SubObject3()->Objects(56)->SubObject1()

Według mnie jest to jednak żadna wada. Podobne potworki świadczą tak naprawdę, że struktura programu jest nieprzemyślana i niezgodna z potrzebami. Nic zatem dziwnego, że korzystanie z niej jest trudne i niewygodne. W tym przypadku można wręcz powiedzieć, że język programowania wykazuje się pewną inteligencją, jednoznacznie wskazując nam, że coś robimy źle ;-)

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


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