Możliwości przeciążania operatorów w C++ dla własnych typów obiektów sprawiają, że mogą one (tj. te obiekty) zachowywać się w bardzo różny sposób. Mogą na przykład “udawać” pewne wbudowane konstrukcje językowe, nierzadko wykonując ich zadania lepiej i wygodniej. Przykładów na to można podać co najmniej kilka – oto one:
()
. Jest on na tyle elastyczny, że może przyjmować dowolne parametry i zwracać dowolne rezultaty, co pozwala nawet na stworzenie więcej niż jednego sposobu “wywoływania” danego obiektu. Jednym z bardziej interesujących zastosowań dla tego operatora jest implementacja w C++ brakującego mechanizmu delegatów, czyli wskaźników na metody obiektów.vector
. Wymagane jest tu przeciążenie operatora indeksowania []
. Daje ono wtedy dostęp do elementów obiektu-pojemnika, który zresztą nie musi być wcale indeksowany liczbami, jak w przypadku tablic wbudowanych (dowodem jest choćby kontener map
). Ograniczeniem jest jedynie to, że indeks może być (naraz) tylko jeden, bo chociaż konstrukcja typu:
jest składniowo najzupełniej poprawna, to działa zupełnie inaczej niż można by było się spodziewać :)
*
(w wersji jednoargumentowej) i ->
. Normalnie te operatory nie mają zastosowania wobec obiektów, ale można nadać im znaczenie. Wtedy też mamy obiekt, który zachowuje się jak wskaźnik, czego przykładem jest choćby auto_ptr
z STL-a czy shared_ptr
z Boosta.if
ów i pętli. Sprytne wskaźniki zwykle to robią, a innymi wartymi wspomnienia obiektami, które też takie zachowanie wykazują, są strumienie z biblioteki standardowej. Jest to spowodowane przeciążeniem operatora logicznej negacji !
oraz konwersji na bool
lub void*
.Reasumując, w C++ dzięki przeciążaniu operatorów możemy nie tylko implementować takie “oczywiste” typy danych jak struktury matematyczne (wektory, macierze, itp.), ale też tworzyć własne, nowe (i lepsze) wersje konstrukcji obecnych w języku. Szkoda tylko, że często jest to wręcz niezbędne, by w sensowny sposób w nim programować ;)
Możliwość używania delegatów w C# to fajna rzecz. Przyjemne jest zwłaszcza definiowanie ich “w locie”, czyli bez konieczności tworzenia zupełnie nowej funkcji. Takiego delegata nazywamy wówczas anonimowym:
Przydaje się to zwłaszcza to podawana różnego rodzaju predykatów do funkcji sortujących lub wyszukujących. Takie nienazwane funkcje są zwykle krótkie i bardzo proste.
A co jeśli jest inaczej?… W szczególności interesująca sytuacja jest wtedy, gdy nasz delegat odwołuje się do zmiennej zewnętrznej – czyli takiej, która nie została w nim zadeklarowana, ale której zasięg zawiera definicję delegata. Oto przykład:
Powyższy kod (skompilowany pod .NET co najmniej 3.0) pokaże 0 i 1, bowiem anonimowy delegat wiązany jest z samą zmienną i
, nie zaś jej wartością w momencie definicji funkcji (czyli 0
). Że zachowanie to nie jest znowu tak oczywiste, można uzasadnić podając przykład biblioteki Boost.Lambda dla C++, gdzie jest odwrotnie. Tam domyślnie wiązane są same wartości zmiennych zewnętrznych (w momencie tworzenia anonimowej funkcji), ale można to zmienić niewielkim wysiłkiem (używając modyfikatora var
, jeśli kogoś to interesuje).
W C# podobnej możliwości nie ma, a do anonimowych delegatów wiązane są zawsze same zmienne, a nie ich wartości. Jeśli jednak potrzebowalibyśmy czegoś takiego, to prostym wyjściem jest wprowadzanie zmiennej pomocniczej, zainicjowanie jej wartością zmiennej pierwotnej i używanie jej wewnątrz delegatu:
Zarówno delegata, jak i deklarację owej zmiennej dobrze jest też zamknąć w osobnym bloku kodu – tak jak powyżej. Dzięki temu eliminujemy możliwość przypadkowej jej modyfikacji, która oczywiście zostałaby “zauważona” przez delegata.
Dzisiaj w programowaniu aplikacji obowiązują dwie proste i fundamentalne zasady. Po pierwsze, programujemy obiektowo i kod zamykamy w klasy z polami i metodami. Po drugie, tworzymy programy działające w środowisku graficznym, z okienkami, przyciskami, polami tekstowymi i innymi klasycznymi elementami interfejsu.
Jednak jeden plus dwa równa się kłopoty, przynajmniej w C++. Już raz narzekałem zresztą na to, że w tym języku obiektowość i GUI to nie jest najlepsze połączenie. Zgrzyta tu mechanizm obsługi zdarzeń generowanych przez interfejs graficzny.
Wcześniej napisałem, że możliwym rozwiązaniem problemu jest zasymulowanie w jakiś sposób delegatów, czyli – z grubsza – wskaźników na metody obiektów. To jedna z dróg radzenia sobie z kwestią obsługi zdarzeń. Ale też inna, wykorzystująca mechanizm metod wirtualnych i polimorfizmu. Polega to na zdefiniowaniu jednolitej klasy bazowej dla obiektów, które mają odbierać zdarzenia. Zwie się je zwykle event handlers, co jak zwykle nie ma dobrego tłumaczenia. Taki handler wyglądać może na przykład tak:
Mając jakiś element interfejsu użytkownika, np. przycisk, przekazujemy mu nasz własny obiekt implementujący powyższy interfejs. Metody tego obiekty są następnie (polimorficznie) wywoływane w reakcji na odpowiednie zdarzenia.
Tak oczywiście można robić w C++, ale nie jest to zbyt wygodne. Tym czego znów brakuje, to niestatyczne klasy wewnętrzne, obecne choćby w Javie, których brak w C++ nie da się do końca zastąpić wielokrotnym dziedziczeniem.
Albo delegaty, albo sposób opisany przed chwilą – zapewne nie ma żadnej innej drogi obiektowej obsługi zdarzeń. Niestety, żaden z tych sposobów nie jest obecnie wspierany w C++ i nie zanosi się, by miało się to wkrótce zmienić.