Używaj rzutowania w stylu C++

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.