Stroustrup mówi: „Nie od razu wymyśl wyjątkową bazę dla wszystkich swoich klas (klasę Object). Zazwyczaj możesz zrobić to lepiej bez wielu / większości klas”. (Czwarta edycja języka programowania C ++, Sec 1.3.4)
Dlaczego generalnie klasa podstawowa jest na ogół złym pomysłem i kiedy warto go stworzyć?
c++
object-oriented
object-oriented-design
Matthew James Briggs
źródło
źródło
Odpowiedzi:
Bo co ten obiekt miałby pod względem funkcjonalności? W java cała klasa Base ma toString, hashCode i równość oraz zmienną warunek monitor +.
ToString przydaje się tylko do debugowania.
hashCode jest użyteczny tylko wtedy, gdy chcesz przechowywać go w kolekcji opartej na haszowaniu (w C ++ preferowane jest przekazywanie funkcji skrótu do kontenera jako parametru szablonu lub unikanie go
std::unordered_*
całkowicie i zamiast tego używaniestd::vector
zwykłych nieuporządkowanych list).równości bez obiektu podstawowego można pomóc w czasie kompilacji, jeśli nie mają tego samego typu, nie mogą być równe. W C ++ jest to błąd czasu kompilacji.
zmienna monitorowania i warunku jest lepiej uwzględniona wyraźnie w poszczególnych przypadkach.
Jednak gdy trzeba zrobić więcej, pojawia się przypadek użycia.
Na przykład w QT istnieje
QObject
klasa główna, która stanowi podstawę powinowactwa wątków, hierarchii własności rodzic-dziecko i mechanizmu szczelin sygnałowych. Wymusza także użycie wskaźnika przez obiekty QObject, jednak wiele klas w Qt nie dziedziczy QObject, ponieważ nie potrzebują one szczeliny sygnałowej (szczególnie typy wartości niektórych opisów).źródło
Object
._hashCode
nie jest „użycie innego kontenera”, ale wskazanie obecnie C ++std::unordered_map
robi haszowanie przy użyciu argumentu szablonu, zamiast wymagać od samej klasy elementów zapewnienia implementacji. Oznacza to, że podobnie jak wszystkie inne dobre kontenery i menedżery zasobów w C ++, nie przeszkadza; nie zanieczyszcza wszystkich obiektów funkcjami lub danymi na wypadek, gdyby ktoś mógł ich później potrzebować w pewnym kontekście.Ponieważ nie ma funkcji wspólnych dla wszystkich obiektów. W tym interfejsie nie ma nic, co miałoby sens dla wszystkich klas.
źródło
Ilekroć budujesz wysokie hierarchie dziedziczenia obiektów, masz tendencję do napotkania problemu Kruchej klasy bazowej (Wikipedia.) .
Posiadanie wielu małych oddzielnych (odrębnych, izolowanych) hierarchii dziedziczenia zmniejsza ryzyko napotkania tego problemu.
Włączenie wszystkich obiektów do jednej hierarchii dziedziczenia o ogromnych rozmiarach praktycznie gwarantuje, że napotkasz ten problem.
źródło
cout.print(x).print(0.5).print("Bye\n")
- nie zależyoperator<<
.Dlatego:
Implementacja dowolnego rodzaju
virtual
funkcji wprowadza wirtualną tabelę, która wymaga narzutu przestrzeni dla poszczególnych obiektów, który nie jest ani konieczny, ani pożądany w wielu (większości?) Sytuacjach.Wdrożenie
toString
pozawirusowe byłoby dość bezużyteczne, ponieważ jedyne, co mógłby zwrócić, to adres obiektu, który jest bardzo nieprzyjazny dla użytkownika i do którego wywołujący już ma dostęp, inaczej niż w Javie.Podobnie niewirtualny
equals
lubhashCode
mógłby używać adresów tylko do porównywania obiektów, co jest znowu całkiem bezużyteczne, a często wręcz wręcz błędne - w przeciwieństwie do Javy, obiekty są często kopiowane w C ++, a zatem odróżnianie „tożsamości” obiektu nie jest nawet zawsze znaczące lub przydatne. (np.int
naprawdę nie powinien mieć tożsamości innej niż jego wartość ... dwie liczby całkowite o tej samej wartości powinny być równe.)źródło
open
słowo kluczowe. Nie sądzę jednak, żeby wykraczało to poza kilka artykułów.shared_ptr<Foo>
aby sprawdzić, czy jest to równieżshared_ptr<Bar>
(lub podobnie z innymi typami wskaźników), nawet jeśliFoo
iBar
są niezwiązanymi klasami, które nic o sobie nie wiedzą. Wymaganie, aby coś takiego działało z „surowymi wskaźnikami”, biorąc pod uwagę historię tego, jak takie rzeczy są używane, byłoby kosztowne, ale w przypadku rzeczy, które i tak będą przechowywane na stosie, dodatkowy koszt byłby minimalny.Posiadanie jednego obiektu głównego ogranicza to, co możesz zrobić i to, co kompilator może zrobić, bez większych korzyści.
Wspólna klasa główna umożliwia tworzenie kontenerów z czegokolwiek i wyodrębnianie ich za pomocą
dynamic_cast
, ale jeśli potrzebujesz kontenerów z czymkolwiek, to coś podobnegoboost::any
może zrobić bez wspólnej klasy root. Aboost::any
także wspiera prymitywów - może nawet wspierać małe optymalizacji bufora i zostawić je prawie „rozpakowanych” w żargonie Java.C ++ obsługuje i rozwija typy wartości. Zarówno literały, jak i typy wartości napisane przez programistę. Kontenery C ++ skutecznie przechowują, sortują, mieszają, konsumują i wytwarzają typy wartości.
Dziedziczenie, a zwłaszcza rodzaj monolitycznego dziedziczenia klas bazowych w stylu Java, wymaga typu „wskaźnikowego” lub „referencyjnego” opartego na wolnym magazynie. Twój uchwyt / wskaźnik / odniesienie do danych zawiera wskaźnik do interfejsu klasy i może polimorficznie reprezentować coś innego.
Chociaż jest to przydatne w niektórych sytuacjach, po ślubie ze wzorcem z „wspólną klasą bazową”, zablokowałeś całą bazę kodu na koszt i bagaż tego wzoru, nawet jeśli nie jest to przydatne.
Prawie zawsze wiesz więcej o typie niż „to obiekt” na stronie wywołującej lub w kodzie, który go używa.
Jeśli funkcja jest prosta, napisanie jej jako szablonu daje polimorfizm oparty na czasie kompilacji typu kaczego, w którym informacje na stronie wywołującej nie są wyrzucane. Jeśli funkcja jest bardziej złożona, można wykonać kasowanie typu, dzięki czemu można zbudować jednolite operacje na typie, który chcesz wykonać (powiedzmy, serializacja i deserializacja) i zapisać (w czasie kompilacji) do użycia (w czasie wykonywania) przez kod w innej jednostce tłumaczeniowej.
Załóżmy, że masz bibliotekę, w której chcesz, aby wszystko można było serializować. Jednym z podejść jest posiadanie klasy podstawowej:
Teraz każdy fragment kodu, który piszesz, może być
serialization_friendly
.Z wyjątkiem nie
std::vector
, więc teraz musisz napisać każdy pojemnik. I nie te liczby całkowite, które otrzymałeś z biblioteki bignum. I nie tego typu, który napisałeś, że nie uważasz, że potrzebujesz serializacji. I nie atuple
,int
ani adouble
, ani astd::ptrdiff_t
.Przyjmujemy inne podejście:
na co składa się, no cóż, pozornie nic nie robienie. Z wyjątkiem teraz możemy rozszerzyć
write_to
, zastępującwrite_to
jako wolną funkcję w przestrzeni nazw typu lub metody w typie.Możemy nawet napisać trochę kodu kasowania typu:
a teraz możemy wziąć dowolny typ i automatycznie umieścić go w ramce w
can_serialize
interfejsie, który umożliwiaserialize
późniejsze wywołanie za pośrednictwem interfejsu wirtualnego.Więc:
jest funkcją, która bierze wszystko, co może serializować, zamiast
i pierwszy, w przeciwieństwie do drugiego, to poradzi sobie
int
,std::vector<std::vector<Bob>>
automatycznie.Nie trzeba było wiele pisać, szczególnie dlatego, że tego rodzaju rzeczy rzadko robisz, ale zyskaliśmy możliwość traktowania wszystkiego jako serializowalnego bez konieczności posiadania podstawowego typu.
Co więcej, możemy teraz nadać możliwość
std::vector<T>
serializacji jako obywatel pierwszej klasy, po prostu nadpisującwrite_to( my_buffer*, std::vector<T> const& )
- z tym przeciążeniem można go przekazać docan_serialize
a serializowalność zapisówstd::vector
jest przechowywana w vtable i dostępna przez.write_to
.Krótko mówiąc, C ++ jest wystarczająco potężny, aby w razie potrzeby wdrożyć zalety jednej klasy bazowej w locie, bez konieczności płacenia ceny hierarchii wymuszonego dziedziczenia, gdy nie jest to wymagane. A czasy, w których wymagana jest pojedyncza baza (podrobiona lub nie), są dość rzadkie.
Gdy typy są w rzeczywistości ich tożsamością i wiesz, czym one są, możliwości optymalizacji są ogromne. Dane są przechowywane lokalnie i przylegle (co jest bardzo ważne dla przyjazności pamięci podręcznej na nowoczesnych procesorach), kompilatory mogą łatwo zrozumieć, co robi dana operacja (zamiast mieć nieprzejrzysty wskaźnik metody wirtualnej, który musi przeskakiwać, prowadząc do nieznanego kodu w po drugiej stronie), co pozwala optymalnie zmienić kolejność instrukcji, a mniej okrągłych kołków wbija się w okrągłe otwory.
źródło
Powyżej znajduje się wiele dobrych odpowiedzi, a wyraźny fakt, że wszystko, co można zrobić z podstawową klasą wszystkich obiektów, można zrobić lepiej na inne sposoby, jak pokazuje odpowiedź @ ratchetfreak i komentarze na jej temat, jest bardzo ważne, ale jest jeszcze jeden powód, którym jest unikanie tworzenia diamentów spadkowychgdy używane jest wielokrotne dziedziczenie. Jeśli posiadasz jakąkolwiek funkcjonalność w uniwersalnej klasie bazowej, jak tylko zaczniesz korzystać z wielokrotnego dziedziczenia, musisz zacząć określać, do którego wariantu chcesz uzyskać dostęp, ponieważ może być inaczej przeciążony w różnych ścieżkach łańcucha dziedziczenia. Baza nie może być wirtualna, ponieważ byłoby to bardzo nieefektywne (wymaganie od wszystkich obiektów posiadania wirtualnej tabeli przy potencjalnie ogromnych kosztach użycia pamięci i lokalizacji). To bardzo szybko stałoby się logistycznym koszmarem.
źródło
W rzeczywistości wczesne kompilatory i biblioteki Microsofts C ++ (wiem o Visual C ++, 16 bitów) miały taką klasę o nazwie
CObject
.Musisz jednak wiedzieć, że w tym czasie „szablony” nie były obsługiwane przez ten prosty kompilator C ++, więc podobne klasy
std::vector<class T>
nie były możliwe. Zamiast tego implementacja „wektorowa” mogła obsłużyć tylko jeden typ klasy, więc istniała klasa porównywalna zstd::vector<CObject>
dzisiejszą. PonieważCObject
była to podstawowa klasa prawie wszystkich klas (niestety nieCString
- odpowiednikstring
współczesnych kompilatorów), możesz użyć tej klasy do przechowywania prawie wszystkich rodzajów obiektów.Ponieważ współczesne kompilatory obsługują szablony, ten przypadek użycia „ogólnej klasy bazowej” nie jest już podawany.
Musisz pomyśleć o tym, że użycie takiej ogólnej klasy bazowej będzie kosztować (trochę) pamięć i czas działania - na przykład w wywołaniu konstruktora. Istnieją więc wady korzystania z takiej klasy, ale przynajmniej przy użyciu nowoczesnych kompilatorów C ++ prawie nie ma przypadku użycia takiej klasy.
źródło
TObject
jeszcze zanim MFC istniało. Nie obwiniaj Microsoftu za tę część projektu, wydawało się, że to dobry pomysł dla prawie wszystkich w tym czasie.Mam zamiar zasugerować inny powód, który pochodzi z Javy.
Ponieważ nie można stworzyć klasy podstawowej dla wszystkiego, przynajmniej nie bez zestawu płyt kotłowych.
Być może uda ci się uciec przed własnymi klasami - ale prawdopodobnie okaże się, że w końcu powielasz dużo kodu. Np. „Nie mogę użyć
std::vector
tutaj, ponieważ nie implementujeIObject
- lepiej utworzę nowy program,IVectorObject
który robi właściwą rzecz ...”.Tak będzie w przypadku, gdy masz do czynienia z wbudowanymi lub standardowymi klasami bibliotek lub klasami z innych bibliotek.
Teraz, jeśli została ona zbudowana na język chcesz skończyć z rzeczy jak
Integer
iint
zamieszanie, które jest w Java lub dużej zmiany w składni języka. (Uważam, że myślę, że niektóre inne języki wykonały dobrą robotę, wbudowując je w każdy typ - ruby wydaje się lepszym przykładem.)Zauważ również, że jeśli twoja klasa podstawowa nie jest polimorficzna w czasie wykonywania (tj. Przy użyciu funkcji wirtualnych), możesz uzyskać taką samą korzyść z używania cech takich jak framework.
np. zamiast
.toString()
ciebie możesz mieć następujące: (UWAGA: Wiem, że możesz to zrobić lepiej, używając istniejących bibliotek itp., to tylko przykładowy przykład.)źródło
Prawdopodobnie „void” spełnia wiele ról uniwersalnej klasy bazowej. Możesz rzucić dowolny wskaźnik na
void*
. Następnie możesz porównać te wskaźniki. Możeszstatic_cast
wrócić do oryginalnej klasy.Jednak nie możesz zrobić z
void
tym, co możesz zrobić,Object
to użyć RTTI, aby dowiedzieć się, jaki typ obiektu naprawdę masz. Jest to ostatecznie spowodowane tym, że nie wszystkie obiekty w C ++ mają RTTI i rzeczywiście można mieć obiekty o zerowej szerokości.źródło
[[no_unique_address]]
, które mogą być używane przez kompilatory do nadania zerowej szerokości podobiektom składowym.[[no_unique_address]]
pozwoli kompilatorowi na zmienne składowe EBO.Java przyjmuje filozofię projektowania, zgodnie z którą Niezdefiniowane zachowanie nie powinno istnieć . Kod taki jak:
przetestuje, czy
felix
posiada podtypCat
tego implementującego interfejsWoofer
; jeśli tak się stanie, wykona rzutowanie i wywołanie,woof()
a jeśli nie, zgłosi wyjątek. Zachowanie kodu jest w pełni zdefiniowane, niezależnie od tego, czyfelix
implementuje,Woofer
czy nie .C ++ przyjmuje filozofię, że jeśli program nie powinien podjąć próby wykonania jakiejś operacji, nie powinno mieć znaczenia, co zrobiłby wygenerowany kod, gdyby ta operacja została podjęta, a komputer nie powinien tracić czasu na próby ograniczenia zachowania w przypadkach, w których „powinien” nigdy nie powstają. W C ++, dodając odpowiednie operatory pośrednie, aby rzutować a
*Cat
na a*Woofer
, kod dałby określone zachowanie, gdy rzutowanie jest zgodne z prawem, ale niezdefiniowane zachowanie, gdy nie jest .Posiadanie wspólnego typu podstawowego dla rzeczy umożliwia sprawdzanie rzutów między pochodnymi tego typu podstawowego, a także wykonywanie operacji try-cast, ale sprawdzanie poprawności rzutów jest droższe niż zakładanie, że są one uzasadnione i mieć nadzieję, że nic złego się nie stanie. Zgodnie z filozofią C ++ taka walidacja wymaga „płacenia za coś, czego [zwykle] nie potrzebujesz”.
Kolejny problem, który dotyczy C ++, ale nie stanowiłby problemu dla nowego języka, polega na tym, że jeśli kilku programistów tworzy wspólną bazę, czerpie z niej swoje własne klasy i pisze kod do pracy z elementami tej wspólnej klasy podstawowej, taki kod nie będzie mógł pracować z obiektami opracowanymi przez programistów, którzy używali innej klasy bazowej. Jeśli nowy język wymaga, aby wszystkie obiekty stosu miały wspólny format nagłówka i nigdy nie dopuścił obiektów stosu, które tego nie zrobiły, wówczas metoda wymagająca odwołania do obiektu stosu z takim nagłówkiem zaakceptuje odwołanie do dowolnego obiektu stosu mógł kiedykolwiek stworzyć.
Osobiście uważam, że wspólny sposób zadawania pytań obiektowi „czy można przekształcić go w typ X” jest bardzo ważną cechą w języku / frameworku, ale jeśli taka funkcja nie jest wbudowana w język od samego początku, trudno jest dodaj to później. Osobiście uważam, że taką klasę podstawową należy przy pierwszej okazji dodać do standardowej biblioteki, z mocnym zaleceniem, aby wszystkie obiekty, które będą używane polimorficznie, powinny dziedziczyć z tej bazy. Posiadanie przez każdego programisty własnych „typów podstawowych” utrudniłoby przekazywanie obiektów między kodami różnych ludzi, ale posiadanie wspólnego typu podstawowego, z którego odziedziczyło wielu programistów, ułatwiłoby to.
UZUPEŁNIENIE
Korzystając z szablonów, można zdefiniować „uchwyt dowolnego obiektu” i zapytać go o typ zawartego w nim obiektu; Pakiet Boost zawiera coś o nazwie
any
. Tak więc, mimo że C ++ nie ma standardowego typu „sprawdzalne odniesienie do czegokolwiek”, można go utworzyć. Nie rozwiązuje to wspomnianego problemu polegającego na tym, że nie ma czegoś w standardzie językowym, tj. Niezgodności między implementacjami różnych programistów, ale wyjaśnia, w jaki sposób radzi sobie C ++ bez podstawowego typu, z którego wszystko się wywodzi: umożliwiając tworzenie coś, co działa jak jeden.źródło
Woofer
jest interfejsem iCat
jest dziedziczny, obsada byłaby uzasadniona, ponieważ mogłaby istnieć (jeśli nie teraz, być może w przyszłości)WoofingCat
dziedziczącaCat
i implementującaWoofer
. Zauważ, że w modelu kompilacji / łączenia Java, utworzenie aWoofingCat
nie wymagałoby dostępu do kodu źródłowego dlaCat
aniWoofer
.Cat
na aWoofer
i odpowie na pytanie „czy można przekształcić w typ X”. C ++ pozwoli ci wymusić obsadę, bo hej, może faktycznie wiesz, co robisz, ale pomoże ci również, jeśli tak naprawdę nie chcesz.dynamic_cast
będzie miało zdefiniowane zachowanie, jeśli wskaże obiekt polimorficzny, i Niezdefiniowane zachowanie, jeśli nie, więc z perspektywy semantycznej ...Symbian C ++ faktycznie miał uniwersalną klasę bazową, CBase, dla wszystkich obiektów, które zachowywały się w określony sposób (głównie jeśli przydzielały stertę). Dostarczył wirtualny destruktor, wyzerował pamięć klasy podczas budowy i ukrył konstruktor kopii.
Uzasadnieniem było to, że był to język dla systemów wbudowanych oraz kompilatorów i specyfikacji C ++, które były naprawdę gówno 10 lat temu.
Nie wszystkie klasy odziedziczyły po tym, tylko niektóre.
źródło