Do każdego aspektu języka, z którego korzystamy, możemy mieć mniejsze lub większe uwagi. Oczywiście również posiadam swoją subiektywną listę niedociągnięć, jeśli chodzi o język C++.
Pomimo tego, że bardzo docieniam polimorfizm i sposób zaimplementowania tego mechanizmu, za niedopracowane uważam kwestie związane z dziedziczeniem i wykorzystaniem funkcji wirtualnych w C++.
Patrzę na takie rzeczy przez pryzmat problemów, które są niepotrzebnie generowane, utrudniając życie programistom. Jednym z moich faworytów jest brak mechanizmu, pozwalającego oznaczać metody wirtualne w klasach pochodnych jako przeciążone.
Problemy z przeciążaniem metod wirtualnych.
Spójrzmy na taką sytuację:
#include <iostream> class ClassA { public: virtual ~ClassA() {} virtual void Function(int a_Argument) { std::cout << "ClassA::Function" << std::endl; } }; class ClassB : public ClassA { public: virtual void Function(long a_Argument) { std::cout << "ClassB::Function" << std::endl; } }; int main() { ClassA* ptrA = new ClassB; ptrA->Function(4); delete ptrA; return 0; }
Czy programista chciał przeciążyć funkcję Function z klasy bazowej ClassA ? Zapewne tak. Czy się udało ? Kod kompiluje się bez problemu, aplikacja również uruchamia się bez błędów. Natomiast na konsoli widzimy
ClassA::Function
zamiast
ClassB::Function
.
Powód ? Nagłówek funkcji Function w klasie pochodnej ma inny typ argumentu, niż wersja bazowa. Czasami bardzo trudno zauważyć takie błędy. Finalnie nie mamy przeciążenia funkcji wirtualnej w klasie pochodnej, a zwykłe przeładowanie nazwy funkcji z inną listą argumentów.
Wszystko jest super pod względem składni języka, ale nie o to nam chodziło…
Jeszcze łatwiejszym do przeoczenia błędem byłoby np. oznaczenie funkcji z klasy bazowej jako const. Dokładnie ta sama sytuacja – przeładowanie zamiast przeciążenia.
Specyfikator override.
Na szczęście definicja języka w wersji C++11 doczekała się zmian również pod tym względem. Jedną z nich jest nowy specyfikator (nie słowo kluczowe !) override. Umożliwia on oznaczenie funkcji w klasie pochodnej jako takiej, która powinna przeciążać funkcję wirtualną z klasy bazowej. Jeśli tak się nie dzieje, kompilator może zareagować odpowiednio do sytuacji.
Weźmy poprzedni przykład:
#include <iostream> class ClassA { public: virtual ~ClassA() {} virtual void Function(int a_Argument) { std::cout << "ClassA::Function" << std::endl; } }; class ClassB : public ClassA { public: virtual void Function(long a_Argument) override { std::cout << "ClassB::Function" << std::endl; } }; int main() { ClassA* ptrA = new ClassB; ptrA->Function(4); delete ptrA; return 0; }
Kod różni się jedynie specyfikatorem override na końcu nagłówka funkcji Function w klasie pochodnej. Po tej zmianie kopilator krzyczy, że funkcja w klasie pochodnej niczego nie przeciąża – a powinna. Mamy więc jasny komunikat o błędzie w kodzie źródłowym.
Po wrowadzeniu niezbędnej poprawki (zmiana typu argumentu z long na int) i uruchomieniu aplikacji, na konsoli widzimy
ClassB::Function
, czyli wynik wg pierwotnego planu.
Podsumowanie.
Specyfikator override to doskonały krok w dobrym kierunku 🙂 Jest to jedna z ciekawszych nowości w C++11, która eliminuje cały zestaw potencjalnie paskudnych błędów, związanych z polimorfizmem.
Jedyne, na co można narzekać, to fakt, że override nie jest obowiązkowe podczas przeciążania funkcji wirtualnej. Powodem jest jednak najprawdopodobniej chęć zachowania kompatybilności wstecz ze starszym kodem, co jest nadal jednym z celów języka.
Kontynuuj poznawanie C++11: C++11 #8: Przeciążanie funkcji wirtualnych: specyfikator final.
Dodatkową zaletą stosowania override jest zabezpieczenie przed usunięciem słowa virtual z klasy bazowej. W takiej sytuacji osoba, która chce to zrobić będzie świadoma, że są istnieją klasy pochodne. Dodatkowym smaczkiem, mniej znanym jest słowo kluczowe final, które także odnosi się do przeciążania metod.
O final będę opowiadał w osobnym wpisie 🙂