Dlaczego w C ++ odradza się bazę dla wszystkich obiektów?

76

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ć?

Matthew James Briggs
źródło
16
ponieważ C ++ nie jest Javą ... I nie powinieneś próbować tego zmuszać.
AK_
10
Pytanie o przepełnienie stosu: Dlaczego w C ++ nie ma klasy podstawowej?
26
Nie zgadzam się również z bliskimi głosami na „głównie oparte na opiniach”. Istnieją bardzo konkretne powody, które można wyjaśnić, ponieważ odpowiedzi potwierdzają zarówno to pytanie, jak i powiązane pytanie SO.
2
Jest to zasada zwinności „nie będziesz jej potrzebować”. Jeśli nie określiłeś już określonej potrzeby, nie rób tego (dopóki tego nie zrobisz).
Jool,
3
@AK_: W twoim komentarzu brakuje „tak głupiego jak”.
DeadMG

Odpowiedzi:

75

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żywanie std::vectorzwykł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 QObjectklasa 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).

maniak zapadkowy
źródło
7
Zapomniałeś wspomnieć prawdopodobnie o głównej przyczynie, dla której Java ma klasę podstawową: Przed generycznymi klasami kolekcji potrzebna była klasa podstawowa do działania. Wszystko (pamięć wewnętrzna, parametry, zwracane wartości) zostało wpisane Object.
Aleksandr Dubinsky,
1
@AleksandrDubinsky: A leki generyczne dodawały tylko cukier składniowy, nie zmieniając niczego poza polską.
Deduplicator
4
Twierdziłbym, że kod skrótu, równość i obsługa monitorów to także błędy projektowe w Javie. Kto uważał, że dobrym pomysłem jest zablokowanie wszystkich obiektów ?!
usr
1
Tak, ale nikt tego nie chce. Kiedy ostatni raz trzeba było zablokować obiekt i nie można było utworzyć oddzielnego obiektu blokady, aby to zrobić. Jest to bardzo rzadkie i obciąża wszystko. Faceci z Jawy źle rozumieli wtedy bezpieczeństwo wątków, czego dowodem są wszystkie obiekty będące blokadą oraz przestarzałe zbiory bezpiecznych wątków. Bezpieczeństwo wątków jest własnością globalną, a nie dla poszczególnych obiektów.
usr
2
hashCode jest użyteczny tylko wtedy, gdy chcesz przechowywać go w kolekcji opartej na haszowaniu (w C ++ preferowane są std :: vector i zwykłe listy nieuporządkowane). ” Prawdziwym kontrargumentem _hashCodenie jest „użycie innego kontenera”, ale wskazanie obecnie C ++ std::unordered_maprobi 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.
underscore_d
100

Ponieważ nie ma funkcji wspólnych dla wszystkich obiektów. W tym interfejsie nie ma nic, co miałoby sens dla wszystkich klas.

DeadMG
źródło
10
+1 za prostotę odpowiedzi, to naprawdę jedyny powód.
BWG
7
W dużych ramach, z którymi mam doświadczenie, wspólna klasa podstawowa zapewnia infrastrukturę do serializacji i refleksji, która jest pożądana w dowolnym kontekście. Meh Po prostu spowodowało to, że ludzie szeregowali szereg cruft razem z danymi i metadanymi, i spowodowało, że format danych był zbyt duży i złożony, aby był wydajny.
dmckee,
19
@dmckee: Twierdzę również, że serializacja i refleksja nie są potrzebami powszechnie użytecznymi.
DeadMG
16
@DeadMG: „ALE CO JEŚLI POTRZEBUJESZ WSZYSTKO?”
deworde
8
Nie wiem, wpisujesz to w cudzysłów, używasz wszystkich wielkich liter, a ludzie nie widzą dowcipu. @MSalters: Cóż, to powinno być łatwe, ma minimalną liczbę stanów, wystarczy określić, że tam jest. Mogę zapisać swoje imię na liście bez wchodzenia w pętlę rekurencyjną.
deworde
25

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.

Mike Nakis
źródło
6
Gdy klasa podstawowa (w języku Java „java.lang.Object”) nie zawiera żadnych metod wywołujących inne metody, problem z niestabilną klasą podstawową nie może wystąpić.
Martin Rosenau,
3
Byłaby to bardzo przydatna klasa podstawowa!
Mike Nakis,
9
@MartinRosenau ... jak możesz to zrobić w C ++ bez potrzeby posiadania podstawowej klasy bazowej!
gbjbaanb
5
@ DavorŽdralo Więc C ++ ma głupią nazwę dla podstawowej funkcji („operator <<” zamiast czegoś sensownego jak „DebugPrint”), podczas gdy Java ma dziwaka klasy podstawowej dla absolutnie każdej klasy, którą piszesz, bez wyjątków. Myślę, że bardziej lubię brodawkę C ++.
Sebastian Redl
4
@ DavorŽdralo: Nazwa funkcji jest nieistotna. Wyobraź sobie składnię cout.print(x).print(0.5).print("Bye\n")- nie zależy operator<<.
MSalters
24

Dlatego:

  1. Nie powinieneś płacić za to, czego nie używasz.
  2. Funkcje te mają mniej sensu w systemie typów opartym na wartościach niż w systemie typów referencyjnym.

Implementacja dowolnego rodzaju virtualfunkcji 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 toStringpozawirusowe 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 equalslub hashCodemó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. intnaprawdę nie powinien mieć tożsamości innej niż jego wartość ... dwie liczby całkowite o tej samej wartości powinny być równe.)

Mehrdad
źródło
W związku z tym problemem i delikatną kwestią dotyczącą klasy bazowej odnotowaną przez Mike'a Nakisa, zwróć uwagę na interesujące badania / propozycje naprawienia go w Javie w zasadzie poprzez uczynienie wszystkich metod wewnętrznymi (tj. Wywołanymi z tej samej klasy) nie-wirtualnymi, ale zachowując ich wirtualne zachowanie, gdy nazywany zewnętrznie; w celu uzyskania starego / standardowego zachowania (tzn. wszędzie wirtualnego) propozycja wprowadziła nowe opensłowo kluczowe. Nie sądzę jednak, żeby wykraczało to poza kilka artykułów.
Fizz,
Nieco więcej dyskusji na temat tego artykułu można znaleźć na stronie lambda-the-ultimate.org/classic/message12271.html
Fizz
Posiadanie wspólnej klasy bazowej pozwoliłoby przetestować dowolną, shared_ptr<Foo> aby sprawdzić, czy jest to również shared_ptr<Bar>(lub podobnie z innymi typami wskaźników), nawet jeśli Fooi Barsą 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.
supercat
Chociaż posiadanie wspólnej klasy podstawowej może nie być pomocne, myślę, że istnieją pewne dość duże kategorie obiektów, dla których pomocne byłyby wspólne klasy podstawowe. Na przykład wiele (znaczna liczba, jeśli nie większość) klas w Javie może być używanych na dwa sposoby: jako nieudostępniony posiadacz danych zmiennych lub jako współdzielony posiadacz danych, których nikt nie może modyfikować. W przypadku obu wzorców użytkowania zarządzany wskaźnik (odniesienie) jest używany jako proxy dla danych bazowych. Pomocne jest posiadanie wspólnego typu zarządzanego wskaźnika dla wszystkich takich danych.
supercat
16

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ś podobnego boost::anymoże zrobić bez wspólnej klasy root. A boost::anytakż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:

struct serialization_friendly {
  virtual void write_to( my_buffer* ) const = 0;
  virtual void read_from( my_buffer const* ) = 0;
  virtual ~serialization_friendly() {}
};

Teraz każdy fragment kodu, który piszesz, może być serialization_friendly.

void serialize( my_buffer* b, serialization_friendly const* x ) {
  if (x) x->write_to(b);
}

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 a tuple, intani a double, ani a std::ptrdiff_t.

Przyjmujemy inne podejście:

void write_to( my_buffer* b, int x ) {
  b->write_integer(x);
}    
template<class T,
  class=std::enable_if_t< void_t<
    std::declval<T const*>()->write_to( std::declval<my_buffer*>()
  > >
>
void write_to( my_buffer* b, T const* x ) {
  if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
  write_to( b, t );
}

na co składa się, no cóż, pozornie nic nie robienie. Z wyjątkiem teraz możemy rozszerzyć write_to, zastępując write_tojako wolną funkcję w przestrzeni nazw typu lub metody w typie.

Możemy nawet napisać trochę kodu kasowania typu:

namespace details {
  struct can_serialize_pimpl {
    virtual void write_to( my_buffer* ) const = 0;
    virtual void read_from( my_buffer const* ) = 0;
    virtual ~can_serialize_pimpl() {}
  };
}
struct can_serialize {
  void write_to( my_buffer* b ) const { pImpl->write_to(b); }
  void read_from( my_buffer const* b ) { pImpl->read_from(b); }
  std::unique_ptr<details::can_serialize_pimpl> pImpl;
  template<class T> can_serialize(T&&);
};
namespace details { 
  template<class T>
  struct can_serialize : can_serialize_pimpl {
    std::decay_t<T>* t;
    void write_to( my_buffer*b ) const final override {
      serialize( b, std::forward<T>(*t) );
    }
    void read_from( my_buffer const* ) final override {
      deserialize( b, std::forward<T>(*t) );
    }
    can_serialize(T&& in):t(&in) {}
  };
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
  std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}

a teraz możemy wziąć dowolny typ i automatycznie umieścić go w ramce w can_serializeinterfejsie, który umożliwia serializepóźniejsze wywołanie za pośrednictwem interfejsu wirtualnego.

Więc:

void writer_thingy( can_serialize s );

jest funkcją, która bierze wszystko, co może serializować, zamiast

void writer_thingy( serialization_friendly const* s );

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ąc write_to( my_buffer*, std::vector<T> const& )- z tym przeciążeniem można go przekazać do can_serializea serializowalność zapisów std::vectorjest 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.

Jak
źródło
8

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.

Jules
źródło
1
Jednym z rozwiązań problemu diamentów jest posiadanie wszystkich typów, które praktycznie nie wyprowadzają typu podstawowego za pomocą wielu ścieżek, zastępując wszystkie wirtualne elementy tego typu podstawowego; jeśli od samego początku wbudowano w język wspólny typ podstawowy, kompilator mógłby automatycznie generować prawidłowe (choć niekoniecznie imponujące) domyślne implementacje.
supercat
5

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 z std::vector<CObject>dzisiejszą. Ponieważ CObjectbyła to podstawowa klasa prawie wszystkich klas (niestety nie CString- odpowiednik stringwspół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.

Martin Rosenau
źródło
3
Czy to MFC? [dopełnienie komentarza]
immibis
3
To jest rzeczywiście MFC. Świecąca latarnia OO, która pokazała światu, jak należy to robić. Och, czekaj ...
gbjbaanb
4
@gbjbaanb Turbo Pascal i Turbo C ++ miały swoje własne TObjectjeszcze zanim MFC istniało. Nie obwiniaj Microsoftu za tę część projektu, wydawało się, że to dobry pomysł dla prawie wszystkich w tym czasie.
hvd
Nawet przed szablonami próba napisania Smalltalk w C ++ przyniosła okropne wyniki.
JDługosz
@hvd Mimo to MFC był znacznie gorszym przykładem projektowania obiektowego niż cokolwiek, co produkował Borland.
Jules
5

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::vectortutaj, ponieważ nie implementuje IObject- lepiej utworzę nowy program, IVectorObjectktó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 Integeri intzamieszanie, 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.)

template<typename T>
struct ToStringTrait;

template<typename T> 
std::string toString(const T & t) {
  return ToStringTrait<T>::toString(t);
}

template<>
struct ToStringTrait<int> {
  std::string toString(int v) {
    return itoa(v);
  }
}

template<typename T>
struct ToStringTrait<std::vector<T>> {
  std::string toString(const std::vector<T> &v) {
    std::stringstream ss;
    ss<<"{";
    for(int i=0; i<v.size(); ++i) {
      ss<<toString(v[i]);
    }
    ss<<"}";
    return ss.str();
  }
}
Michael Anderson
źródło
3

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żesz static_castwrócić do oryginalnej klasy.

Jednak nie możesz zrobić z voidtym, co możesz zrobić, Objectto 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.

pjc50
źródło
1
Tylko podobiekty klasy podstawowej o zerowej szerokości, a nie normalne.
Deduplicator
@Deduplicator W ramach aktualizacji C ++ 17 dodaje [[no_unique_address]], które mogą być używane przez kompilatory do nadania zerowej szerokości podobiektom składowym.
underscore_d
1
@underscore_d Masz na myśli planowane dla C ++ 20, [[no_unique_address]]pozwoli kompilatorowi na zmienne składowe EBO.
Deduplicator
@Deduplicator Ups, tak. Już zacząłem używać C ++ 17, ale myślę, że nadal uważam, że jest bardziej nowatorski niż jest w rzeczywistości!
underscore_d
2

Java przyjmuje filozofię projektowania, zgodnie z którą Niezdefiniowane zachowanie nie powinno istnieć . Kod taki jak:

Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();

przetestuje, czy felixposiada podtyp Cattego implementującego interfejs Woofer; 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, czy feliximplementuje, Wooferczy 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 *Catna 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.

supercat
źródło
Ta rzutowanie kończy się niepowodzeniem w czasie kompilacji w C ++ , Java i C # .
milleniumbug
1
@milleniumbug: Jeśli Wooferjest interfejsem i Catjest dziedziczny, obsada byłaby uzasadniona, ponieważ mogłaby istnieć (jeśli nie teraz, być może w przyszłości) WoofingCatdziedzicząca Cati implementująca Woofer. Zauważ, że w modelu kompilacji / łączenia Java, utworzenie a WoofingCatnie wymagałoby dostępu do kodu źródłowego dla Catani Woofer.
supercat
3
C ++ ma dynamic_cast , który poprawnie obsługuje próbę rzutowania z a Catna a Wooferi 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.
Rob K
2
@RobK: Oczywiście masz rację co do składni; Mea culpa. Czytałem trochę więcej o dynamic_cast i wydaje się, że w pewnym sensie współczesne C ++ ma wszystkie obiekty polimorficzne pochodzące z podstawowej klasy bazowej „obiekt polimorficzny” z dowolnymi polami potrzebnymi do identyfikacji typu obiektu (zazwyczaj vtable wskaźnik, chociaż jest to szczegół implementacji). C ++ nie opisuje w ten sposób klas polimorficznych, ale przekazanie wskaźnika do dynamic_castbędzie miało zdefiniowane zachowanie, jeśli wskaże obiekt polimorficzny, i Niezdefiniowane zachowanie, jeśli nie, więc z perspektywy semantycznej ...
supercat
2
... wszystkie obiekty polimorficzne przechowują pewne informacje w tym samym układzie i wszystkie obsługują zachowanie, które nie jest obsługiwane przez obiekty niepolimorficzne; w moim odczuciu oznacza to, że zachowują się tak, jakby wywodziły się ze wspólnej podstawy, niezależnie od tego, czy definicja języka używa takiej terminologii, czy nie.
supercat
1

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.

James
źródło