Posts tagged ‘overloading’

Jedna funkcja w wielu wersjach

2011-03-10 18:12

Wytykanie niedoskonałości i różnego rodzaju felerów języka C++ to jedno z ulubionych zajęć niektórych programistów, zwłaszcza tych którzy z jakichś powodów muszą z tego języka korzystać. Sam czasami to robię, ale dzisiaj chcę zwrócić uwagę na to, co C++ robi dobrze – albo przynajmniej lepiej niż w wielu innych, popularnych językach. A chodzi tu o swobodę w definiowaniu wielu wersji tej samej funkcji, różniących się listą parametrów. Nazywa się to zwykle przeciążaniem, chociaż nie jest to jedyny sposób na osiągnięcie takiego efektu.

Innym są bowiem parametry domyślne i istnieje przynajmniej jeden język, w którym jest to sposób jedyny. Tak jest bowiem w Pythonie i nawet czasami ma to sens, biorąc pod uwagę brak deklarowanych typów w tym języku. Jednak równie często wymaga to czegoś w stylu samodzielnego rozstrzygania przeciążania już wewnątrz funkcji:

  1. def some_func(string_or_list):
  2.     if isinstance(string_or_list, list): x = "".join(string_or_list)
  3.     elif isinstance(string_or_list, basestring): x = string_or_list
  4.  
  5.     # ...

Powyższe ify to nic innego jak boilerplate code: redundantne, powtarzalne fragmenty kodu, które w dodatku robią tutaj to, co w innych językach spoczywa na barkach kompilatora. Usprawiedliwieniem może być pewnie to, że przecież Python nie jest kompilowany ;P

Ale jak wspomniałem wcześniej, język spod znaku węża posiada możliwość definiowania argumentów domyślnych, za co należy mu oddać honor. Niestety nie da się tego powiedzieć o kilku innych, popularnych dziś językach, jak choćby C# i Javie. Jeśli chcemy mieć w nich dwie sygnatury tej samej metody, musimy ją przeciążyć. Otrzymujemy wtedy dwie wersje (lub więcej), z których jedna – ta posiadająca mniej argumentów – jest po prostu wywołaniem drugiej. Widuje się to często w konstruktorach, z których czasem układa się swego rodzaju sztafeta wywołań:

  1. public Foo() { this(0); }
  2. public Foo(int x) { this(x, null); }
  3. public Foo(int x, Object o) { this(x, o, ""); }
  4. public Foo(int x, Object, String s) {
  5.     // właściwy konstruktor
  6. }

Jak łatwo zauważyć, jest ona wybitnie rozwlekła, trudna w utrzymaniu i modyfikacji, a także mało czytelna, gdyż wartości domyślne argumentów są ukryte w wywołaniach zamiast w deklaracjach funkcji. Jej odpowiednik z argumentami domyślnymi byłby natomiast krótki, przejrzysty, zrozumiały i łatwy do ewentualnej zmiany.

“Problem” z tworzeniem wielu wersji funkcji polega właśnie na tym, iż mimo istnienia dwóch sposobów na osiągnięcie tego samego efektu, bardzo często jeden jest wyraźnie lepszy niż drugi. Stąd każdy język nieposiadający wsparcia dla któregoś z nich niejako z automatu kreuje sytuacje, dla których rozwiązania trzeba szukać na około.
I tu pozytywnie wyróżnia się C++, który pozwala na stosowanie obu technik (przeciążania lub argumentów domyślnych) w zależności od potrzeb. Również razem, jeśli rezultat nie powoduje niejednoznaczności w wywołaniach. Z pewnością jest to cenna cecha tego języka, równoważąca znaczącą część jego znanych niedostatków :)

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

Co może udawać obiekt w C++

2010-05-06 14:56

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:

  • Obiekt może zachowywać się jak funkcja, czyli udostępniać możliwość “wywołania” siebie z określonymi parametrami. Takie twory często nazywa się funktorami i bywają używane podczas pracy ze standardową biblioteką STL.
    Działają one przy tym w bardzo prosty sposób, zwyczajnie przeciążając operator nawiasów okrągłych (). 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.
  • Na podobnej zasadzie obiekt może udawać tablicę – wie o tym każdy, kto choć raz używał klas STL typu 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:
    1. v[3,4] = 5;

    jest składniowo najzupełniej poprawna, to działa zupełnie inaczej niż można by było się spodziewać :)

  • Obiekty mogą też przypominać wskaźniki, które wtedy określa się mianem ‘sprytnych’ (smart pointers). Dzieje się tak głównie za sprawą przeciążenia operatorów * (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.
  • Wreszcie, obiekt może też działać jako flaga boolowska i być używany jako część warunków 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ć ;)

Typ zwracany funkcji wirtualnej

2009-08-04 16:42

Kiedy w klasie pochodnej piszemy nową wersję funkcji wirtualnej, to w C++ powinniśmy zawsze uważać na to, żeby jej nagłówek był identyczny z tym, który zdeklarowano w klasie bazowej. To dlatego, że składnia języka nie wymaga w tej sytuacji żadnego słowa kluczowego w rodzaju override. Jeśli więc coś nieopatrznie zmienimy, to nie uzyskamy nowej wersji funkcji, tylko zupełnie nową funkcję przeciążoną:

  1. class Foo
  2. {
  3.     public: virtual void Do(int x, int y, int z) { }
  4. };
  5.  
  6. class Bar : public Foo
  7. {
  8.     // to *nie* jest nowa wersja Foo::Do, tylko zupełnie inna funkcja
  9.     public: void Do(int x, int y, string z) { }
  10. };

Taki efekt jest jest w gruncie rzeczy oczywisty i łatwy do zrozumienia. Pewnie więc będzie nieco zaskakujące dowiedzieć się, że tak naprawdę postać funkcji wirtualnej w klasie pochodnej może się trochę różnić, a mimo to wciąż będziemy mówili o tej samej funkcji.

Jak to jest możliwe?… Dopuszcza się mianowicie różnicę w typie zwracanym przez nową wersję funkcji wirtualnej. Może on być inny od typu zwracanego przez wersję z klasy bazowej – ale tylko pod warunkiem, że:

  • pierwotny typ był wskaźnikiem lub referencją do klasy (a nie chociażby typem podstawowym, jak int)
  • nowy typ zwracany jest – odpowiednio – wskaźnikiem lub referencją do klasy pochodnej od tej z typu pierwotnego
  • nowy typ posiada tę samą kwalifikację const/volatile co typ pierwotny

Przykładowo więc, jeśli w klasie bazowej funkcja wirtualna zwraca const A*, to w klasie pochodnej może zwracać const B*, o ile klasa B dziedziczy publicznie po A. Nie może za to zwracać samego B* (niestałego) lub const X*, gdy klasa X jest niezwiązana z A.

Do czego taki “myk” może się przydać? Najczęściej chodzi tutaj o sytuacje, gdy mamy do czynienia z równoległym dziedziczeniem kilku klas, które na każdym poziomie są związane ze sobą. Mogę sobie na przykład wyobrazić ogólny menedżer zasobów w grze, którego metoda wirtualna Get zwraca wskaźnik na bazową klasę Resource, a następnie bardziej wyspecjalizowany TextureManager, który w tej sytuacji podaje nam wskaźnik na Texture. (Oczywiście klasa Texture w założeniu dziedziczy tu po Resource). Czasami coś takiego może być potrzebne, aczkolwiek takie równoległe hierarchie dziedziczenia nie są specjalnie eleganckie ani odporne na błędy.
Lepszym przykładem jest wirtualny konstruktor kopiujący: metoda, która pozwala na wykonanie kopii obiektu bez dokładnej znajomości jego typu. Zwyczajowo nazywa się ją Clone:

  1. class Object
  2. {
  3.     public:
  4.         virtual Object* Clone() const { return new Object; }
  5. };
  6.  
  7. class Foo : public Object
  8. {
  9.     public:
  10.         // nowa wersja metody Object::Clone
  11.         Foo* Clone() const { return new Foo; }
  12. };

Dzięki temu że metoda jest wirtualna, można ją wywołać nie znając rzeczywistego typu obiektu (co nie jest możliwe w przypadku zwykłych konstruktorów, które w C++ wirtualne być nie mogą). W wyniku jej wywołania otrzymamy jednak tylko ogólny wskaźnik Object* na powstałą kopię obiektu.
Gdybyśmy teraz nie zmienili typu zwracanego przez metodę w klasie pochodnej, to klonowanie podobne do tego:

  1. void Function(Foo* foo)
  2. {
  3.     Foo* copy = foo->Clone();
  4.     // ...
  5. }

wymagałoby dodatkowego rzutowania w górę, by mogło zostać przepuszczone przez kompilator.

Tags: , , , , ,
Author: Xion, posted under Programming » Comments Off on Typ zwracany funkcji wirtualnej

Przeciążanie przecinka

2008-07-31 16:34

Przecinek to taki niepozorny znak, którego grubo ponad 90% zastosowań ogranicza się zapewne do oddzielania parametrów funkcji (czy to w deklaracji, czy to w wywołaniu). Niektóre języki używają też go do separowania indeksów wielowymiarowych tablic, ale do nich C++ akurat nie należy.
Zamiast tego przecinek jest tam operatorem, o czym rzadko się pamięta. Być może dlatego, że ma on najniższy priorytet spośród wszystkich operatorów. Wydaje się też, że domyślne jego działanie (obliczenie wszystkich argumentów, a następnie zwrócenie ostatniego) nie jest specjalnie przydatne – jeśli w ogóle.

Można je jednak zmienić. Tak, tak – operator przecinka można przeciążać, jakkolwiek zaskakujące i przekombinowane by się to wydawało. W rzeczywistości jest on bowiem zwyczajnym operatorem binarnym o łączności lewostronnej i wspomnianym najniższym priorytecie. A przeciążyć można go, pisząc funkcję operator,() – dokładnie tak samo, jak każdego innego dwuargumentowego operatora.
Do czego może to służyć? Otóż względnie typową sztuczką jest użycie przedefiniowanego przecinka do konstrukcji obiektów złożonych z wielu elementów, np. wektorów lub macierzy. Oto przykład przeciążenia dla standardowej klasy vector:

  1. #include <vector>
  2. template <typename T> std::vector<T> operator , (std::vector<T> v, const T& obj)
  3. {
  4.     v.push_back (obj);
  5.     return v;
  6. }

Pozwala to na inicjalizację wektora w taki oto nietypowy sposób:

  1. std::vector<int> ints = (std::vector<int>(), 1, 2, 3, 4, 5);

Pierwszy element tego ciągu (pusty wektor) jest konieczny, gdyż w przeciwnym razie użyty zostałby standardowy operator przecinka.

Na pierwszy rzut może to wyglądać efektownie. Pamiętajmy jednak, że przecinek ten pozostaje wciąż przecinkiem, zachowując chociażby swoją główną cechę szczególną, czyli priorytet. W szczególności próba dodania do wektora kolejnych elementów w ten sposób:

  1. ints = ints, 6, 7, 8, 9;

zakończy się niezupełnie po naszej myśli…
Mimo to być może warto spróbować wymyślić dla tego operatora jakieś własne, w miarę sensowne zastosowanie. Możliwe, że okaże się on wcale nie tak nieużyteczny, na jakiego zdaje się wyglądać :]

Tags: , ,
Author: Xion, posted under Programming » 4 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
 


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