Dlaczego w C ++ nie ma klasy bazowej?

84

Szybkie pytanie: z punktu widzenia projektowania, dlaczego w C ++ nie ma klasy podstawowej, która jest matką wszystkiego, co zwykle jest objectw innych językach?

Slezica
źródło
27
To bardziej kwestia filozoficzna niż projektowa. Krótka odpowiedź brzmi: „ponieważ Bjarne najwyraźniej nie chciał tego zrobić”.
Jerry Coffin
4
Nic nie stoi na przeszkodzie, abyś go stworzył.
GWW
22
Osobiście uważam, że wadą projektową jest posiadanie wszystkiego pochodzącego z tej samej klasy bazowej. Promuje styl programowania, który powoduje więcej problemów niż jest to warte (ponieważ masz tendencję do posiadania ogólnych kontenerów obiektów, z których musisz następnie skorzystać, aby użyć rzeczywistego obiektu (to marszczy brwi jako zły projekt IMHO)). Czy naprawdę chcę mieć kontener, który może pomieścić zarówno samochody, jak i sterować obiektami automatyki?
Martin York,
3
@Martin, ale spójrz na to w ten sposób: na przykład, aż do C ++ 0x auto, trzeba było używać definicji typu na kilometr dla iteratorów lub jednorazowych typedefs. W przypadku bardziej ogólnej hierarchii klas można po prostu użyć objectlub iterator.
Slezica
9
@Santiago: Zastosowanie ujednoliconego systemu typów prawie zawsze oznacza, że ​​otrzymujesz dużo kodu, który opiera się na polimorfizmie, dynamicznej wysyłce i RTTI, z których wszystkie są stosunkowo drogie i wszystkie hamują optymalizacje, które są możliwe, gdy są nie są używane.
James McNellis

Odpowiedzi:

116

Ostateczne orzeczenie znajduje się w FAQ Stroustrupa . Krótko mówiąc, nie przekazuje żadnego znaczenia semantycznego. Będzie to miało swój koszt. Szablony są bardziej przydatne w przypadku kontenerów.

Dlaczego C ++ nie ma uniwersalnej klasy Object?

  • Nie jest nam potrzebny: programowanie ogólne zapewnia w większości przypadków alternatywy bezpieczne dla typów statycznych. Inne sprawy są obsługiwane przy użyciu dziedziczenia wielokrotnego.

  • Nie ma użytecznej uniwersalnej klasy: prawdziwie uniwersalna klasa nie posiada własnej semantyki.

  • Klasa „uniwersalna” zachęca do niechlujnego myślenia o typach i interfejsach oraz prowadzi do nadmiernego sprawdzania w czasie wykonywania.

  • Użycie uniwersalnej klasy bazowej pociąga za sobą koszt: obiekty muszą być przydzielane do sterty, aby były polimorficzne; co oznacza koszt pamięci i dostępu. Obiekty sterty w naturalny sposób nie obsługują semantyki kopiowania. Obiekty sterty nie obsługują prostego zachowania o określonym zakresie (co komplikuje zarządzanie zasobami). Uniwersalna klasa bazowa zachęca do korzystania z dynamic_cast i innych sprawdzeń w czasie wykonywania.

Kapitanie Żyrafa
źródło
83
Nie potrzebujemy żadnego obiektu klasy bazowej / nie potrzebujemy / kontroli myśli ...;)
Piskvor opuścił budynek
3
@Piskvor - LOL Chcę zobaczyć teledysk!
Byron Whitlock
10
Objects must be heap-allocated to be polymorphic- Myślę, że to stwierdzenie nie jest ogólnie poprawne. Zdecydowanie możesz utworzyć instancję klasy polimorficznej na stosie i przekazać ją jako wskaźnik do jednej z jej klas bazowych, uzyskując zachowanie polimorficzne.
Byron
5
W Javie próba rzutowania odniesienia do - Foona odniesienie do -Bar gdy Barnie dziedziczy Foo, deterministycznie zgłasza wyjątek. W C ++ próba rzucenia wskaźnika na Foo na wskaźnik na Bar może wysłać robota w przeszłość, aby zabić Sarah Connor.
supercat
3
@supercat Dlatego używamy dynamic_cast <>. Aby uniknąć SkyNet i tajemniczo pojawiających się sof Chesterfield.
Captain Giraffe
44

Najpierw zastanówmy się, dlaczego chcesz mieć klasę bazową. Przychodzi mi do głowy kilka różnych powodów:

  1. Do obsługi operacji ogólnych lub kolekcji, które będą działać na obiektach dowolnego typu.
  2. Włączanie różnych procedur, które są wspólne dla wszystkich obiektów (np. Zarządzanie pamięcią).
  3. Wszystko jest przedmiotem (bez prymitywów!). Niektóre języki (takie jak Objective-C) nie mają tego, co sprawia, że ​​rzeczy są dość chaotyczne.

Są to dwa dobre powody, dla których języki marki Smalltalk, Ruby i Objective-C mają klasy bazowe (technicznie rzecz biorąc, Objective-C tak naprawdę nie ma klasy bazowej, ale pod każdym względem ma).

W przypadku # 1 potrzeba klasy bazowej, która ujednolica wszystkie obiekty w jednym interfejsie, została wyeliminowana przez włączenie szablonów w C ++. Na przykład:

void somethingGeneric(Base);

Derived object;
somethingGeneric(object);

jest niepotrzebne, gdy można zachować integralność typu przez cały czas za pomocą polimorfizmu parametrycznego!

template <class T>
void somethingGeneric(T);

Derived object;
somethingGeneric(object);

W przypadku # 2, podczas gdy w Objective-C procedury zarządzania pamięcią są częścią implementacji klasy i są dziedziczone z klasy bazowej, zarządzanie pamięcią w C ++ jest wykonywane przy użyciu kompozycji, a nie dziedziczenia. Na przykład możesz zdefiniować inteligentne opakowanie wskaźnika, które będzie wykonywać zliczanie referencji na obiektach dowolnego typu:

template <class T>
struct refcounted
{
  refcounted(T* object) : _object(object), _count(0) {}

  T* operator->() { return _object; }
  operator T*() { return _object; }

  void retain() { ++_count; }

  void release()
  {
    if (--_count == 0) { delete _object; }
  }

  private:
    T* _object;
    int _count;
};

Wtedy zamiast wywoływać metody w samym obiekcie, będziesz wywoływał metody w jego opakowaniu. Pozwala to nie tylko na bardziej ogólne programowanie: pozwala również na oddzielenie problemów (ponieważ w idealnym przypadku obiekt powinien być bardziej zainteresowany tym, co powinien robić, niż tym, jak jego pamięć powinna być zarządzana w różnych sytuacjach).

Wreszcie, w języku, który ma zarówno prymitywy, jak i rzeczywiste obiekty, takie jak C ++, korzyści z posiadania klasy bazowej (spójny interfejs dla każdej wartości) są tracone, ponieważ wtedy masz pewne wartości, które nie mogą być zgodne z tym interfejsem. Aby użyć prymitywów w takiej sytuacji, musisz podnieść je do obiektów (jeśli Twój kompilator nie zrobi tego automatycznie). Stwarza to wiele komplikacji.

A więc krótka odpowiedź na twoje pytanie: C ++ nie ma klasy bazowej, ponieważ mając polimorfizm parametryczny za pośrednictwem szablonów, nie musi.

Jonathan Sterling
źródło
W porządku! Cieszę się, że okazało się to pomocne :)
Jonathan Sterling
2
W przypadku punktu 3 liczę za pomocą C #. C # ma prymitywy, które można zapakować w object( System.Object), ale nie muszą. Do kompilatora inti System.Int32są aliasami i mogą być używane zamiennie; w razie potrzeby środowisko wykonawcze obsługuje boks.
Cole Johnson,
W C ++ nie ma potrzeby boksu. W przypadku szablonów kompilator generuje poprawny kod w czasie kompilacji.
Rob K
2
Zwróć uwagę, że chociaż ta (stara) odpowiedź przedstawia inteligentny wskaźnik liczony jako odwołanie, C ++ 11 ma teraz, std::shared_ptrktórego należy użyć zamiast tego.
15

Dominującym paradygmatem dla zmiennych C ++ jest przekazywanie przez wartość, a nie przekazywanie przez odniesienie. Wymuszenie wyprowadzenia wszystkiego z korzenia Objectspowodowałoby, że przekazywanie ich przez wartość byłoby błędem ipse facto.

(Ponieważ zaakceptowanie obiektu według wartości jako parametru, z definicji spowodowałoby wycięcie go i usunięcie jego duszy).

To jest niepożądane. C ++ sprawia, że ​​myślisz o tym, czy potrzebujesz semantyki wartości, czy referencji, dając ci wybór. To wielka rzecz w obliczeniach wydajnościowych.

sehe
źródło
1
To nie byłby sam w sobie błąd. Hierarchie typów już istnieją i w razie potrzeby całkiem dobrze wykorzystują przekazywanie wartości. Jednak zachęcałoby to do stosowania strategii opartych na stertach, w których bardziej odpowiednie jest przekazywanie przez odniesienie.
Dennis Zickefoose
IMHO, dobry język powinien mieć różne rodzaje dziedziczenia w sytuacjach, w których klasa pochodna może być legalnie używana jako obiekt klasy bazowej przy użyciu wycinka według odniesienia, gdzie można ją przekształcić w jedną za pomocą wycinka po wartości, i te, w których żadne z nich nie jest uzasadnione. Wartość posiadania wspólnego typu podstawowego dla rzeczy, które można kroić, byłaby ograniczona, ale wielu rzeczy nie można przecinać i muszą być przechowywane pośrednio; dodatkowy koszt posiadania takich rzeczy pochodzących ze wspólnego Objectbyłby stosunkowo niewielki.
supercat
6

Problem w tym, że taki typ JEST w C ++! Tak jest void. :-) Dowolny wskaźnik może być bezpiecznie niejawnie rzutowany navoid * , w tym wskaźniki do typów podstawowych, klas bez wirtualnej tabeli i klas z wirtualną tabelą.

Ponieważ powinien być kompatybilny ze wszystkimi tymi kategoriami obiektów, voidsam w sobie nie może zawierać metod wirtualnych. Bez funkcji wirtualnych i RTTI nie można uzyskać żadnych przydatnych informacji o typie void(pasuje do KAŻDEGO typu, więc można powiedzieć tylko rzeczy, które są prawdziwe dla KAŻDEGO typu), ale funkcje wirtualne i RTTI sprawiłyby, że proste typy byłyby bardzo nieskuteczne i powstrzymałyby C ++ przed byciem język odpowiedni do programowania niskopoziomowego z bezpośrednim dostępem do pamięci itp.

Więc jest taki typ. Po prostu zapewnia bardzo minimalistyczny (w rzeczywistości pusty) interfejs ze względu na niskopoziomowy charakter języka. :-)

Ellioh
źródło
Typy wskaźników i typy obiektów nie są takie same. Nie możesz mieć obiektu typu void.
Kevin Panko
1
W rzeczywistości tak zwykle działa dziedziczenie w C ++. Jedną z najważniejszych rzeczy, które robi, jest zgodność typów wskaźników i referencji: tam, gdzie wymagany jest wskaźnik do klasy, można podać wskaźnik do klasy pochodnej. A zdolność do static_cast wskaźników między dwoma typami jest znakiem, że są one połączone w hierarchii. Z tego punktu widzenia void jest zdecydowanie uniwersalną klasą bazową w C ++.
Ellioh,
Jeśli zadeklarujesz zmienną typu, voidnie będzie ona kompilowana i otrzymasz ten błąd . W Javie możesz mieć zmienną typu Objecti to zadziała. Na tym polega różnica między voidtypem „prawdziwym” a. Być może to prawda, że voidjest to typ bazowy dla wszystkiego, ale ponieważ nie zawiera żadnych konstruktorów, metod ani pól, nie ma sposobu, aby stwierdzić, czy istnieje, czy nie. Tego twierdzenia nie można udowodnić ani obalić.
Kevin Panko,
1
W Javie każda zmienna jest odniesieniem. To jest różnica. :-) (BTW, w C ++ są też klasy abstrakcyjne, których nie można używać do deklarowania zmiennej). W mojej odpowiedzi wyjaśniłem, dlaczego nie można uzyskać RTTI z pustki. Zatem teoretycznie void nadal jest klasą bazową wszystkiego. static_cast jest dowodem, ponieważ wykonuje rzutowania tylko między pokrewnymi typami i może być używane do void. Ale masz rację, brak dostępu RTTI w pustce naprawdę bardzo różni się od typów podstawowych w językach wyższego poziomu.
Ellioh,
1
@Ellioh Po zapoznaniu się ze standardem C ++ 11 §3.9.1p9, przyznaję, że voidjest to typ (i odkryłem dość sprytne użycie? : ), ale wciąż jest daleko od bycia uniwersalną klasą bazową. Po pierwsze jest to „niepełny typ, którego nie można ukończyć”, a po drugie, nie ma void&typu.
bcrist
-2

C ++ jest językiem silnie typizowanym. Zastanawiające jest jednak to, że nie ma ona uniwersalnego typu obiektu w kontekście specjalizacji szablonowej.

Weźmy na przykład wzór

template <class T> class Hook;
template <class ReturnType, class ... ArgTypes>
class Hook<ReturnType (ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

który można utworzyć jako

Hook<decltype(some_function)> ...;

Teraz załóżmy, że chcemy tego samego dla określonej funkcji. Lubić

template <auto fallback> class Hook;
template <auto fallback, class ReturnType, class ... ArgTypes>
class Hook<ReturnType fallback(ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

z wyspecjalizowaną instancją

Hook<some_function> ...

Ale niestety, nawet jeśli klasa T może zastępować dowolny typ (klasa lub nie) przed specjalizacją, nie ma odpowiednika auto fallback(używam tej składni jako najbardziej oczywistej generycznej innej niż typ w tym kontekście), który mógłby zastąpić dowolny argument szablonu innego niż typ przed specjalizacją.

Więc ogólnie ten wzorzec nie przenosi z argumentów szablonu typu do argumentów szablonu innego niż typ.

Podobnie jak w przypadku wielu zakrętów w języku C ++, odpowiedź prawdopodobnie brzmi „żaden członek komitetu o tym nie pomyślał”.

user7339377
źródło
Nawet jeśli (coś w rodzaju) ReturnType fallback(ArgTypes...)zadziałałoby, byłby to zły projekt. template <class T, auto fallback> class Hook; template <class ReturnType, class ... ArgTypes> class Hook<ReturnType (ArgTypes...), ReturnType(*fallback)(ArgTypes...)> ...robi to, co chcesz i oznaczają parametry szablonu Hooksą spójne rodzaju
Caleth
-4

C ++ początkowo nosił nazwę „C z klasami”. Jest to rozwinięcie języka C, w przeciwieństwie do innych bardziej nowoczesnych rzeczy, takich jak C #. I nie można zobaczyć C ++ jako języka, ale jako podstawę języków (tak, pamiętam książkę Scotta Meyersa Efektywne C ++).

Sam C jest mieszanką języków, języka programowania C i jego preprocesora.

C ++ dodaje kolejną mieszankę:

  • podejście klasowe / obiektowe

  • szablony

  • plik STL

Osobiście nie lubię niektórych rzeczy, które pochodzą bezpośrednio z C do C ++. Jednym z przykładów jest funkcja wyliczenia. Sposób, w jaki C # pozwala deweloperowi na jego użycie, jest o wiele lepszy: ogranicza wyliczenie w swoim własnym zakresie, ma właściwość Count i jest łatwo iterowalny.

Ponieważ C ++ chciał być retrokompatybilny z C, projektant był bardzo liberalny, pozwalając językowi C na wejście w całości do C ++ (są pewne subtelne różnice, ale nie pamiętam nic, co można by zrobić za pomocą kompilatora C, nie można zrobić przy użyciu kompilatora C ++).

sergiol
źródło
„nie widzę C ++ jako języka, ale jako podstawy języków” ... czy chcesz rzeczywiście wyjaśnić i zilustrować, do czego prowadzi to dziwne stwierdzenie? Wiele paradygmatów różni się od „wielu języków”, chociaż można śmiało powiedzieć, że preprocesor jest odłączony od pozostałych kroków kompilatora - dobra uwaga. Ponowne wyliczenia - w razie potrzeby można je w trywialny sposób opakować w zakres klas, a w całym języku brakuje introspekcji - jest to poważny problem, chociaż w tym przypadku można go przybliżyć - zobacz kod benum Boost vault. Czy chodzi tylko o pytanie: C ++ nie zaczynał się od uniwersalnej bazy?
Tony Delroy,
@ Tony: Co ciekawe, książka, do której się odwołałem, jedyną rzeczą, o której nawet nie wspominają, ponieważ jest innym językiem, jest preprocesor, który jest jedynym, który rozważasz osobno. Rozumiem twój punkt widzenia, ponieważ preprocesor najpierw analizuje własne dane wejściowe. Ale posiadanie mechanizmu tworzenia szablonów jest jak posiadanie innego języka, który dokonuje uogólnienia za Ciebie. Stosujesz składnię szablonu i będziesz mieć funkcje generujące kompilator dla każdego posiadanego typu. Kiedy możesz mieć program napisany w C i przekazać jego dane wejściowe kompilatorowi C ++, są to dwa języki w jednym: C i C z obiektami => C ++
sergiol Kwietnia
STL, może być postrzegany jako dodatek, ale ma bardzo praktyczne klasy i kontenery, które poprzez szablony NATYWNIE rozszerzają możliwości C ++. A wyliczenia, o których mówiłem, są NATIVE wyliczeniami. Kiedy mówisz o Boost, polegasz na kodzie innej firmy, który nie jest NATYWNY dla języka. W języku C # nie muszę uwzględniać niczego zewnętrznego, aby mieć iterowalne wyliczenie o zakresie własnym. W C ++ sztuczka, której często używam, polega na wywołaniu ostatniego elementu wyliczenia - bez przypisanych wartości, więc są one automatycznie uruchamiane od zera i zwiększane - coś w rodzaju NUM_ITEMS, co daje mi górną granicę.
sergiol
2
dziękuję za dalsze wyjaśnienia - przynajmniej widzę, skąd przychodzisz; Słyszałem kilka osób narzekających, że nauka korzystania z szablonów wydawała się rozłączna z innymi programami w C ++, chociaż z pewnością nie jest to moje doświadczenie. Pozostawiam innym czytelnikom, co zechcą, z innych perspektyw, które przedstawisz. Twoje zdrowie.
Tony Delroy,
1
Myślę, że największym problemem związanym z posiadaniem uniwersalnego typu podstawowego jest to, że taki typ byłby bezużyteczny, gdyby nie miał żadnych metod wirtualnych; C ++ nie chciał dodawać żadnych narzutów dla typów struktur, które nie mają żadnych metod wirtualnych, ale każdy obiekt dowolnego typu, który ma jakiekolwiek metody wirtualne, musi mieć co najmniej wskaźnik do tabeli wysyłkowej. Gdyby klasy i struktury zamieszkiwały różne wszechświaty, możliwe byłoby posiadanie uniwersalnego obiektu klasy, ale klasy i struktury są w zasadzie tym samym w C ++.
supercat