Chociaż temat ten był wałkowany m.in. przez Scotta Meyers'a w More Effective C++, to wciąż widzi się masę kodu pisanego z rzutowaniami w stylu C. Dla przypomnienia, w C++ mamy cztery rodzaje rzutowań:
static_cast
const_cast
dynamic_cast
reinterpret_cast
Rzutowania w stylu C wyglądają natomiast tak: (typ)argument
, lub tak: typ(argument)
.
Jeden z ciekawych problemów, który może się pojawić u programisty stosującego rzutowania w stylu C ilustruje poniższy fragment kodu:
Object Foo( int(parameter) );
Ten kod wbrew pozorom nie robi tego, na co wygląda. Nie jest to stworzenie obiektu Foo typu Object, z rzutowaniem parametru na typ int
. C++ przyjmuje zasadę, że wszystko co wygląda jak deklaracja funkcji ma być w pierwszej kolejności potraktowane jak deklaracja funkcji. I w tym przypadku kod ten deklaruje nam funkcję o nazwie Foo, zwracającej obiekt typu Object i przyjmującej jeden parametr typu int
.
Może na co dzień nie spotkamy się z takimi ekstremalnymi zjawiskami, ale rzutowania w stylu C++ mają kilka dobrych cech, o których warto wiedzieć:
- Są dużo bardziej przyjazne dla grep'a i innych narzędzi wyszukujących w tekście. O wiele łatwiej jest znaleźć wszystkie wystąpienia
static_cast
niż rzutowań opartych o nawiasy okrągłe, które czasem mogą przypominać np. wywołanie funkcji albo tworzenie nowego obiektu. Poza tym, w jaki sposób znaleźć wszystkie rzutowania niezależnie od typu, na który się ono odbywa? ;)
- Są dużo bardziej przyjazne dla vgrep'a (dla ludzkiego oka ;) ) - łatwiej je wypatrzeć w gąszczu nawiasów; z resztą w większości IDE są kolorowane jako słowa kluczowe. Weźmy przykład poniższego kodu:
int offset = (int)(T*)1 - (int)(Singleton <T>*)(T*)1
ms_singleton = (T*)((int)this + offset);
i porównajmy go z tym:
int offset = reinterpret_cast<int>(reinterpret_cast<T*>(1)) -
reinterpret_cast<int>(static_cast<Singleton <T>*>(reinterpret_cast<T*>(1)));
ms_singleton = reinterpret_cast<T*>((reinterpret_cast<int>(this) + offset));
Wariant C++ nie jest ani trochę piękniejszy, ale przynajmniej można od razu policzyć ile jest rzutowań i jakiego rodzaju. Poza tym, składnia wywołania funkcji pozwala od razu ogarnąć, które wyrażenia są rzutowane.
- Jak widać powyżej, są długie, rozwlekłe i brzydkie. I bardzo dobrze. Rzutowania to coś, czego nie powinno się stosować bez wyraźnej potrzeby, więc jeśli programiście przeszkadza pisanie takiego kodu, to tym lepiej dla projektu.
- Lepiej rozdzielają odpowiedzialność - każdy z rodzajów rzutowania w C++ jest odpowiedzialny za inne zadanie. Dzięki temu kod lepiej wyraża intencje autora (np. widząc w kodzie
const_cast
masz pewność, że programista chciał jedynie zdjąć lub założyć modyfikator const
albo volatile
).
- Mają spójną składnię. Rzutowania w stylu C mogą występować w dwóch wariantach - z typem w nawiasie oraz w składni "konstruktora", z rzutowanym argumentem w nawiasie. Sam fakt, że w tym samym programie możemy natknąć się na oba warianty powoduje, że myśl o wyszukiwaniu ich w kodzie jest przerażająca.
Zainteresowanych tematem rzutowania odsyłam do świetnego artykułu Reg'a poświęconego temu tematowi. Chciałbym zaznaczyć jednak, że nie zgadzam się z jego wnioskiem numer 2, tj.
Nie ma praktycznych przesłanek, by rezygnować ze stosowania eleganckiego, zwięzłego rzutowania w stylu C na rzecz rozwlekłych operatorów static_cast i reinterpret_cast.
Wydaje mi się, że w tym poście zawarłem wiele praktycznych przesłanek za rezygnacją z rzutowania w stylu C.