To ja jeszcze dodam, że wywołanie metody wirtualnej to zwykle jedna, dwie /wielodziedziczenie itd./ instrukcje procesora. Dla aktualnych procesorów w optymalnych warunkach ta róznica nie istnieje, call to call, i tak trzeba potoki opróżnić, a że jest ich sporo to mogą niwelować efekty istnienia tej dodatkowej instrukcji, zreszta często jest wykonywana równolegle z poprzednimi. Jak mam być szczery to xnacznie większy narzut daje chociaż minimalnie źle zrobiona pętla. Do tego cstringi na stosie np. - kompilator ma obowiązek stworzyć tablicę na stosie i ją zainicjować, a to boli zdecydowanie bardziej niż polimorfizm. Kolejna sprawa - pakowanie danych w strukturach - wyrównanie pól, tak samo wyrównanie stosu czy danych ogólnie, to wszystko się ustawia. Wyrównanie ma straszny wpływ na wydajność, ze względu na cache. Znasz wszystkie cechy platformy, na którą piszesz? Dam głowę, że nie wiesz o opóźnieniach generowanych przez użycie shortów... ale z drugiej strony shorty będą szybsze od intów przy większych tablicach - zdecydowanie mniej wymiany cache-RAM. Nawet takie signed\unsigned ma wpływ... Procesory wykonują jeżeli tylko mogą po kilka instrukcji jednocześnie - wpasuj sobie tutaj narzut późnego wiązania. W skrajnym wypadku jak nie robisz nic więcej jak tylko wołasz w pętli metody stracisz może kilka procent, przy bardziej skomplikowanych operacjach narzut jset w promilach bądź nie istnieje. Ważniejszy jest algorytm. Tak, faktyczny czas wykonywania zależy od złożoności. Denerwują mnie 'programiści C++' liczący każdy cykl procka - to tylko dowód nikłego pojęcia o rzeczy. Co do nadmiarowości kodu - nie zgodzę się, przy nadużywaniu OOP jest wiele 'zbędnego' kodu, ale temu jest winien przede wszystkim programista. Hm, tak, zabawki Borlanda i biblioteka VCL - przykład jak nie używać OOP.
Po pierwsze - OOP poprawia znacząco organizację kodu, zapewnia możilwość jego wielokrotnego wykorzystania przy odpowiednim zaprojektowaniu, znacząco kod skraca i upraszcza.
Wydaje mi sie to być tworzeniem "na siłę" kodu zorientowanego obietkowo, a przecież odwoływanie się do funkcji, która zawiera sie w 10 klasach chyba nie jest bez znaczenia na szybkość działania programu?
Wywołanie metody z którejś z wyższych klas w drzewie to zwykle zwykłe wywołanie funkcji + ew. korekcja thisa - to tak jakbyś do globalnej funkcji przekazał strukturę przez wskaźnik jawnie. Przy dziedziczeniu wirtualnym, bardziej złożonej hierarchii klas dochodzi czasem wspomniana korekcja ale to znów tak jak w przypadku metod wirtualnych niezauważalny narzut. Takie kopiowanie metod jak wspomniałeś pomijając straszne koszty konserwacji i ryzyko błędów mają jeszcze jedną poważną wadę - znacząco powiększają program, tak kod źródłowy jak i maszynowy. Obie cechy są niekorzystne.
Funkcje wirtualne mają za zadanie zapewnić jednakowe działanie metody dla każdego poziomu hierarchii klas - takie, jak zdefiniowała najniższa zmieniająca działanie klasa w drzewie. Co to daje za wyjątkiem polimorfizmu na klasie bazowej? Ano to, że metody z klasy bazowej mogą używać metod wirtualnych, np. akcesorów - klasa bazowa implementuje całą mechanikę niezależną, wejśćie\wyjście definiują klasy pochodne. Cały vic polega na tym, że klasa bazowa może robić ciekawe rzeczy nie przejmując się tym czy czyta\zapisuje dane w pamięci, pliku czy może przez sockety. Jaki jest efekt? Powiedzmy, że piszesz klasę protokołu komunikacyjnego, pobiera i wysyła pojedyncze bajty... ale sposób komunijacji może być skrajnie róźny - dzięki metodom wirtualnym masz całą mechnikę napisaną raz, obojętne czy używasz portu równoległego, szeregowego, głośniczka systemowego czy interface'u do ekologicznych znaków dymnych - implementujesz tylko dwie metody + konstruktor dla każdego nowego sposobu komunikacji. Jak jakiś buc rozgryzie nasze cudowne szyfrowanie xor'em to powodzenia w dodawaniu negocjacji klucza i szyfrowania asymetrycznego w Bóg wie ilu klasach, tak żeby wszystko chciało działać. Powiedz klientowi, że w zamian za kolejne 6 m-cy wymagania programu będą oscylować w okolicach Pentium 90MHz zamiast 100MHz, powinien się ucieszyć ;-P
Dziedziczenie i komponowanie obiektów wydatnie skraca czas tworzenia oprogramowania, podnosi jego niezawodność itd.
Co do wirtualności wszystkich funkcji - wirtualne powinny być tylko te, których zmiana zachowania jest sensowna, te które określają zachowanie specjalizacji klasy.
A propos 'niepotrzebnych' pól klas - dziedziczysz także metody i całą mechanikę, jeżeli w klasie, z której dziedziczysz te pola były używane to i klasa pochodna ich będzie używać. Zresztą - bardzo duża klasa to błąd projektowy, każda klasa powinna wykonywać tylko jedno ściśle określone zadanie.
Wiesz, to zakrawa na hipokryzję - przejmujesz się kilkoma polami, które może nie będą potrzebne a chcesz kopiować całą impelemntację klas nadrzędnych. Płaczesz nad kilkoma 'marnowanymi' bajtami marnując całe kilobajty.
generalnie wydaje mi sie ze szybkość programu(optymalizacja) ma większy priorytet niż czytelność kodu...
Dziedziczenie jest szybsze niż kopiowanie, to raz - ta sama metoda klasy bazowej jest wołana dla klasy pochodnej - kłania się cache procesora. Co do czytelności, 'napisz i zapomnij' to niezbyt dobry pomysł jeżeli masz zamiar jeszcze do kodu zaglądać - piszesz w języku wysokiego poziomu do diabła, część kompilatorów wspiera nawet generowanie kodu podczas linkowania, napisany przez Ciebie kod nie ma bezpośredniego przełożenia na maszynowy. Ty implementujesz algorytmy + IO i trochę niskiej mechaniki, kompilator robi resztę. Mały przykład, nieco 'funkcyjny':
#include <iostream>
#include <functional>
using namespace std;
const unsigned uLipotes = 0x44;
const unsigned uVexillifer = 01;
int main() {
cout << &uLipotes << " " << &uVexillifer << endl;
cout << bind1st(plus<unsigned>(), uLipotes) (uVexillifer) << endl;
return 0;
}
Co to robi? Wypisuje adresy stałych /żeby wymusić ich obecność/ i za pomocą obiektów funkcyjnych oblicza ich sumę... jak powinno wyglądać najbardziej interesujące nas wyrażenie? Tworzony jest obiekt klasy plus, po konkretyzacji szablonu, oferuje on operator(), następnie idzie do konstruktora bind1st - kolejny obiekt funkcyjny, posiada jednoargumentowy operator() i związane w sobie argumenty z konstruktora, które w operatorze () łączy i wywołuje. W końcu jawne wywołanie metody bind1st::operator()(unsigned), które z kolei robi niejawne wywołanie metody plus<unsigned>::operator()(unsigned, unsigned). I co? Powinno gdyby kompilator bezpośrednio to na binarkę przekładał mieć narzut większy niż Visual Basic ale, z debuggera, standardowe ustawienia przy kompilacji:
cout << bind1st(plus<unsigned>(), uLipotes) (uVexillifer) << endl;
004016EE push 45h
004016F0 call std::basic_ostream<char,std::char_traits<char> >::operator<< (401810h)
Czyli? Ano całość zostałą przekształcona i obliczona podczas kompilacji. Wnioski? Ty piszesz, kompilator robi po swojemu. Przykład może niezbyt sensowny ale ilustruje dobrze jakie różnice powstają w kodzie.
Mały apel - ludzie, nie myślcie niskopoziomowo tam, gdzie nie jest to wymagane. Nie tylko utrudnia to tworzenie kodu ale i potrafi się negatywnie na jego jakości odbić. O wydajności myśli się kiedy ma się już rozwiązanie problemu, i tylko wtedy gdy aktualna jest niewystarczająca. W pierwszej kolejności optymalizacja powinna polegać na zmianie algorytmu, mniejsza złożoność jest ważniejsza. Mówiąc krótko - optymalizować z głową, tylko gdy zapewni to sensowne korzyści.
Hm, tak, miałem pracować ;-P