Чому база "для всіх об'єктів" відбивається в C ++


76

Stroustrup каже: "Не одразу вигадуйте унікальну базу для всіх своїх класів (клас" Object "). Як правило, ви можете обійтися без неї для багатьох / більшості класів." (Четверта редакція мови програмування C ++, розділ 1.3.4)

Чому взагалі базовий клас на все погана ідея, і коли має сенс його створити?


16
тому що C ++ - це не Java ... І не варто намагатися примушувати це бути.
AK_

10
На питання про переповнення стека: Чому в C ++ немає базового класу?

26
Крім того, я не погоджуюся з близькими голосами за "насамперед на основі думки". Існують дуже конкретні причини, які можуть бути пояснені цим, оскільки відповіді засвідчують як на це питання, так і пов'язане питання ОУ.

2
Це спритний принцип "вам це не знадобиться". Якщо ви вже не визначили певну потребу в цьому, не робіть цього (поки ви цього не зробите).
Jool

3
@AK_: У вашому коментарі відсутнє "настільки ж дурне, як".
DeadMG

Відповіді:


75

Бо який би цей об’єкт мав для функціональності? У Java всі класу Base - це toString, хеш-код і рівність та змінна умова монітора +.

  • ToString корисний лише для налагодження.

  • hashCode корисний лише в тому випадку, якщо ви хочете зберігати його в колекції на основі хешу (перевага в C ++ полягає в тому, щоб передати хеш-функцію контейнеру як параметр шаблону або std::unordered_*взагалі уникати і замість цього використовувати std::vectorзвичайні і не упорядковані списки).

  • рівність без базового об'єкта може допомогти під час компіляції, якщо вони не мають одного типу, то вони не можуть бути рівними. У C ++ це помилка часу компіляції.

  • змінна монітора та стану краще включатись явно в кожному конкретному випадку.

Однак, коли є більше, що потрібно зробити, то є випадок використання.

Наприклад, у QT є кореневий QObjectклас, який є основою спорідненості потоку, ієрархії власності батько-дитина та механізму слотів сигналу. Він також змушує використовувати покажчик для QObjects, однак багато класів у Qt не успадковують QObject, оскільки у них немає потреби в слоті сигналу (особливо типи значень деякого опису).


7
Ви забули згадати, мабуть, головну причину, що у Java є базовий клас: перед тим, як генеричні класи, колекції колекції потребували базового класу, щоб функціонувати. Було набрано все (внутрішнє зберігання, параметри, значення повернення) Object.
Олександр Дубінський,

1
@AleksandrDubinsky: І дженерики лише додавали синтаксичний цукор, насправді не змінюючи нічого, крім польського.
Дедуплікатор

4
Я заперечую, хеш-код, рівність і підтримка монітора - це також помилки в дизайні в Java. Хто думав, що було б гарною ідеєю зробити всі об’єкти замком ?!
usr

1
Так, але ніхто цього не хоче. Коли в останній раз вам потрібно було заблокувати об'єкт, і ви не можете створити окремий замок для цього. Це дуже рідко, і це накладає тягар на все. Хлопці з Java погано розуміли безпеку ниток тоді як свідчення того, що всі об'єкти є замком та колекції безпечних ниток, які тепер застаріли. Безпека ниток - це глобальна властивість, а не об'єкт.
usr

2
" hashCode корисний лише в тому випадку, якщо ви хочете зберігати його в колекції на основі хешу (перевага в C ++ призначено для std :: векторних та простих не упорядкованих списків). " Справжній контраргумент _hashCodeне "використовувати інший контейнер", а навпаки виходить, що C ++ std::unordered_mapробить хешування з використанням аргументу шаблону, замість того, щоб вимагати реалізації саме клас класу елементів. Тобто, як і всі інші хороші контейнери та керування ресурсами в C ++, це не нав'язливо; він не забруднює всі об'єкти функціями або даними на випадок, якщо пізніше комусь вони потрібні.
підкреслюй_d

100

Тому що немає функцій, якими поділяються всі об'єкти. У цей інтерфейс немає нічого, що має сенс для всіх класів.


10
+1 для простоти відповіді, це справді єдина причина.
BWG

7
У великих рамках, з якими я маю досвід, загальний базовий клас забезпечує інфраструктуру серіалізації та відображення, бажану в контексті <що б там не було>. Мех. Це призвело до того, що люди серіалізували купу сукупності разом із даними та метаданими та зробили формат даних занадто великим та складним, щоб бути ефективним.
dmckee

19
@dmckee: Я також заперечую, що серіалізація та рефлексія навряд чи є загальнокорисними потребами.
DeadMG

16
@DeadMG: "АЛЕ ЩО МОЖЕТЕ ЗБЕРІГАТИ ВСЕ?"
deworde

8
Я не знаю, ти це вкладаєш у лапки, ти використовуєш усі шапки, і люди не бачать жарту. @MSalters: Ну, це повинно бути легко, він має мінімальний стан, ви просто вкажіть, що він є. Я можу записати своє ім’я у список, не вводячи рекурсивний цикл.
deworde

25

Щоразу, коли ви будуєте високі ієрархії спадкування об'єктів, ви, як правило, стикаєтеся з проблемою Крихкого базового класу (Вікіпедія.) .

Наявність безлічі невеликих окремих (відокремлених, ізольованих) ієрархій успадкування зменшує шанси натрапити на цю проблему.

Здійснення всіх ваших об'єктів частиною єдиної ієрархічної спадкової спадщини практично гарантує, що ви зіткнетеся з цією проблемою.


6
Коли базовий клас (на Java "java.lang.Object") не містить жодних методів, які викликають інші методи, проблема неміцного базового класу не може виникнути.
Мартін Розенау

3
Можливий корисний базовий клас, який був би!
Майк Накіс

9
@MartinRosenau ... як це можна робити в C ++, не потребуючи базового базового класу!
gbjbaanb

5
@ DavorŽdralo Отже, C ++ має тупу назву основної функції ("оператор <<" замість чогось розумного, як "DebugPrint"), тоді як у Java є виродком базового класу для абсолютно кожного класу, який ви пишете, без винятку. Я думаю, що мені більше подобається бородавка C ++.
Себастьян Редл

4
@ DavorŽdralo: Назва функції не має значення. Зображте синтаксис cout.print(x).print(0.5).print("Bye\n")- він не зачіпається operator<<.
MSalters

24

Оскільки:

  1. Ви не повинні платити за те, що не використовуєте.
  2. Ці функції мають менший сенс у системі типу на основі значень, ніж у системі типового типу.

Реалізація будь- якої virtualфункції вводить віртуальну таблицю, для якої потрібен накладний простір на об'єкт, який не потрібен і не бажаний у багатьох (більшості?) Ситуаціях.

Реалізація toStringневіртуально буде досить марною, тому що єдине, до чого він може повернутись, - це адреса об'єкта, яка дуже непривітлива для користувачів і до якої абонент вже має доступ, на відміну від Java.
Аналогічно, невіртуальний equalsабо hashCodeміг використовувати лише адреси для порівняння об'єктів, що знову ж таки є досить марним і часто навіть відвертим неправильним - на відміну від Java, об'єкти часто копіюються в C ++, а значить, і розрізнення "ідентичності" об'єкта навіть не є завжди значущий або корисний. (наприклад, intдійсно не повинно бути ідентичності, крім його значення ... два цілих числа одного і того ж значення повинні бути рівними.)


Що стосується цього питання та крихкої проблеми базового класу, відміченої Майком Накісом, відзначте цікаве дослідження / пропозицію виправити його на Java в основному, зробивши всі методи внутрішньо (тобто коли вони викликаються з одного класу) невіртуальними, але зберігаючи свою віртуальну поведінку, коли зовні називається; щоб отримати стару / стандартну поведінку (тобто віртуальний скрізь), пропозиція ввела нове openключове слово. Я не думаю, що це пішло десь за рамками кількох паперів.
Фіз

Трохи більше обговорення цього документу можна знайти на lambda-the-ultimate.org/classic/message12271.html
Fizz

Наявність загального базового класу дозволило б протестувати будь-кого, shared_ptr<Foo> щоб побачити, чи він також є shared_ptr<Bar>(або аналогічно з іншими типами вказівників), навіть якщо Fooі Barне пов'язані між собою класи, які нічого не знають один про одного. Вимагати, щоб така річ працювала з «сировинними покажчиками», враховуючи історію використання таких речей, було б дорого, але для речей, які все одно зберігатимуться в купі, додаткова вартість буде мінімальною.
supercat

Хоча може бути корисним мати загальний базовий клас для всього, але я думаю, що є деякі досить великі категорії об'єктів, для яких загальні базові класи були б корисними. Наприклад, багато (чимала множина, якщо не більшість) класів на Java можуть використовуватися двома способами: як нерозподілений власник даних, що змінюються, або як спільний власник даних, який ніхто не може змінювати. З обома моделями використання керований покажчик (посилання) використовується як проксі для базових даних. Можливість мати загальний керований тип вказівника для всіх таких даних корисно.
supercat

16

Наявність одного кореневого об'єкта обмежує те, що можна робити і що може робити компілятор, без особливих окупностей.

Звичайний кореневий клас дає змогу створювати контейнери з чого-небудь і витягувати ті, що вони є dynamic_cast, а якщо вам потрібні контейнери-що-небудь, то щось подібне boost::anyможе зробити це без загального кореневого класу. А boost::anyтакож підтримує примітиви - він навіть може підтримувати оптимізацію невеликих буферів і залишати їх майже "без коробки" в Java Java.

C ++ підтримує та процвітає у значеннях типів. Як літеральні, так і програмні типи письмових значень. Контейнери C ++ ефективно зберігають, сортують, хешують, споживають та виробляють типи цінностей.

Наслідування, особливо вид монолітного успадкування базових класів стилю Java, вимагає типів "покажчик" або "еталон" у вільному магазині. Ваша ручка / покажчик / посилання на дані містить покажчик на інтерфейс класу, і поліморфно може представляти щось інше.

Незважаючи на те, що це корисно в деяких ситуаціях, коли ви одружилися за зразком із "загальним базовим класом", ви зафіксували всю свою кодову базу у вартості та багажі цього зразка, навіть коли це не корисно.

Майже завжди ви знаєте більше про тип, ніж "це об'єкт" або на викличному сайті, або в коді, який його використовує.

Якщо функція проста, написання функції в якості шаблону дає вам тип качки, який базується на поліморфізмі, що базується на часі, коли інформація на виклику не викидається. Якщо функція є більш складною, можна стерти тип стирання, завдяки якому одноманітні операції над типом, який ви хочете виконати (скажімо, серіалізація та десеріалізація), можна будувати і зберігати (під час компіляції), щоб споживати (під час виконання) код в іншому підрозділі перекладу.

Припустимо, у вас є бібліотека, де ви хочете, щоб усе було серіалізаційним. Один із підходів - мати базовий клас:

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

Тепер кожен шматочок коду, який ви пишете, може бути serialization_friendly.

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

За винятком не std::vector, тому тепер потрібно писати кожен контейнер. І не ті цілі числа, які ви отримали з цієї бібліотеки bignum. І не той тип, про який ви писали, що не думаєте, що потрібна серіалізація. І не a tuple, intабо a double, a , a std::ptrdiff_t.

Ми використовуємо інший підхід:

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 );
}

яка складається з, ну, нічого не роблячи, здавалося б. За винятком тепер, ми можемо розширити write_to, замінивши write_toяк вільну функцію в просторі імен типу або методу в типі.

Ми навіть можемо написати код стирання типу:

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) );
) {}

і тепер ми можемо прийняти довільний тип і автоматично встановити його в can_serializeінтерфейс, який дозволяє вам serializeв подальшому викликати віртуальний інтерфейс.

Тому:

void writer_thingy( can_serialize s );

це функція, яка бере все, що може серіалізувати замість

void writer_thingy( serialization_friendly const* s );

і перше, в відміну від других, він може працювати int, std::vector<std::vector<Bob>>автоматично.

Написати це не знадобилося багато, тим більше, що подібні речі - це те, що ви рідко хочете робити, але ми отримали можливість трактувати що-небудь як серіалізаційне, не вимагаючи базового типу.

Більше того, тепер ми можемо зробити std::vector<T>серіалізацію як першокласного громадянина просто переосмисленням write_to( my_buffer*, std::vector<T> const& )- з цією перевантаженням вона може бути передана на a can_serializeі серіалізаційність отриманих даних, що std::vectorзберігаються у vtable і доступ до них .write_to.

Коротше кажучи, C ++ є достатньо потужним, що ви можете реалізовувати переваги одного базового класу на ходу, коли це потрібно, без необхідності платити ціну ієрархії примусового спадкування, коли цього не потрібно. А часи, коли потрібна єдина база (підроблена чи ні), досить рідкісні.

Коли типи фактично є їх ідентичністю, і ви знаєте, що вони є, можливостей для оптимізації є багато. Дані зберігаються локально та безперервно (що дуже важливо для зручності кешу на сучасних процесорах), компілятори можуть легко зрозуміти, що робить дана операція (замість того, щоб мати непрозорий віртуальний вказівник методу, який він повинен перескочити, ведучи до невідомого коду на інша сторона), яка дає змогу впорядкування оптимально впорядкувати, і менше круглих кілочок забиваються у круглі отвори.


8

Вище є багато хороших відповідей, і очевидний факт, що все, що ви робите з об'єктами базового класу всіх об'єктів, можна зробити краще іншими способами, як це показано у відповіді @ ratchetfreak та коментарях до нього дуже важливо, але є ще одна причина, яка полягає у тому, щоб уникнути створення спадкових алмазівколи використовується багаторазове успадкування. Якщо у вас був якийсь функціонал у універсальному базовому класі, як тільки ви почали використовувати багатократне успадкування, вам слід було б вказати, до якого варіанту його ви хочете отримати доступ, оскільки він може бути перевантажений по-різному в різних контурах ланцюга спадкування. І база не може бути віртуальною, оскільки це було б дуже неефективно (вимагаючи, щоб усі об'єкти мали віртуальну таблицю з потенційно величезною вартістю використання пам'яті та локальності). Це дуже швидко стане логічним кошмаром.


1
Одне рішення алмазної проблеми полягає у тому, щоб усі типи, які практично не виводять базовий тип за допомогою декількох шляхів, перекривають усі віртуальні члени цього базового типу; якщо загальний базовий тип був вбудований в мову з самого початку, компілятор може автоматично генерувати легітимні (хоча і не обов'язково вражаючі) реалізації за замовчуванням.
supercat

5

Насправді ранні компілятори та бібліотеки Microsofts (я знаю про 16-бітовий Visual C ++) мали такий клас з назвою CObject.

Однак ви повинні знати, що в той час "шаблони" не підтримувалися цим простим компілятором C ++, тому класи на кшталт std::vector<class T>не були можливими. Натомість "векторна" реалізація могла б обробляти лише один тип класу, тому існував клас, порівнянний із std::vector<CObject>сьогоднішнім. Оскільки CObjectбазовий клас майже всіх класів (на жаль, не CString- еквівалент stringсучасних компіляторів), ви могли використовувати цей клас для зберігання майже всіх видів об'єктів.

Оскільки сучасні компілятори підтримують шаблони, цей випадок використання "загального базового класу" більше не дається.

Ви повинні задуматися над тим, що використання такого загального базового класу коштуватиме (небагато) пам'яті та часу виконання - наприклад, при виклику до конструктора. Таким чином, є недоліки при використанні такого класу, але принаймні при використанні сучасних компіляторів C ++ майже немає випадків використання для такого класу.


3
Це MFC? [коментар padding]
immibis

3
Це дійсно MFC. Сяючий маяк OO-дизайну, який показав світові, як слід робити. О, зачекайте ...
gbjbaanb

4
@gbjbaanb Turbo Pascal і Turbo C ++ мали своє, TObjectперш ніж MFC навіть існував. Не звинувачуйте Microsoft у цій частині дизайну, здається, що це була гарна ідея майже всім у той час.
hvd

Ще до того, як шаблони, намагаючись записати Smalltalk на C ++, дали жахливі результати.
JDługosz

@hvd Все-таки MFC був набагато гіршим прикладом об'єктно-орієнтованого дизайну, ніж все, що виробляв Borland.
Жуль

5

Я збираюся запропонувати ще одну причину, яка походить від Java.

Тому що ви не можете створити базовий клас для всього, принаймні, не без набору котлів.

Можливо, ви зможете піти з ним на власні заняття - але, ймовірно, виявите, що в кінцевому підсумку дублюєте багато коду. Наприклад, "я не можу використовувати std::vectorтут, оскільки він не реалізовує IObject- я б краще створити нове похідне, IVectorObjectщо робить правильну справу ...".

Це буде так, коли ви маєте справу з вбудованими або стандартними бібліотечними класами або класами інших бібліотек.

Тепер , якщо він був побудований на мову ви б в кінцевому підсумку з речами , як Integerі intплутанини , яка знаходиться в Java, або велика зміна в синтаксисі мови. (Зверніть увагу, я думаю, що деякі інші мови зробили гарну роботу, вбудувавши їх у кожен тип - рубін здається кращим прикладом.)

Також зауважте, що якщо ваш базовий клас не є поліморфним під час виконання (тобто з використанням віртуальних функцій), ви можете отримати таку ж користь від використання таких ознак, як фреймворк.

наприклад, замість .toString()вас може бути наступне: (ПРИМІТКА. Я знаю, що ви можете зробити це вподобано, використовуючи наявні бібліотеки тощо. Це лише наочний приклад.)

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();
  }
}

3

Можливо, «нікчемність» виконує безліч ролей універсального базового класу. Ви можете кинути будь-який вказівник на a void*. Потім ви можете порівняти ці вказівники. Ви можете static_castповернутися до початкового класу.

Однак те, що ти не можеш зробити, з тим, voidщо ти можеш зробити, Object- це використовувати RTTI, щоб визначити, який тип об’єкта у тебе є. Це в кінцевому рахунку зводиться до того, як не всі об'єкти в C ++ мають RTTI, і дійсно можливо об'єкти нульової ширини.


1
Тільки суб'єкти базової ширини нульової ширини, не нормальні.
Deduplicator

@Deduplicator За допомогою оновлення додається C ++ 17 [[no_unique_address]], яка може використовуватися компіляторами для надання нульової ширини суб’єктам членів.
підкреслюй_d

1
@underscore_d Ви маєте на увазі запланований C ++ 20, [[no_unique_address]]дозволить компілятору використовувати EBO-змінні.
Дедуплікатор

@Deduplicator Whoops, юп. Я вже почав використовувати C ++ 17, але, мабуть, все ще думаю, що це більш сучасне, ніж є насправді!
підкреслюй_d

2

Java бере філософію дизайну, що Undefined Behavior не повинна існувати . Код, такий як:

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

перевірить, чи felixє підтип Catцього інтерфейсу Woofer; якщо це так, він виконуватиме команду і викликає, woof()а якщо цього не зробить, викине виняток. Поведінка коду повністю визначена, felixреалізується Wooferчи ні .

C ++ бере філософію, що якщо програма не повинна намагатися виконати якусь операцію, це не має значення, що згенерований код буде робити, якби ця операція була зроблена, і комп'ютер не повинен витрачати час, намагаючись стримувати поведінку у випадках, які "повинні" ніколи не виникають. У C ++, додаючи відповідні оператори таким чином , непрямого надання кинути *Catдо *Woofer, код буде давати певну поведінку , коли приведення є законним, але невизначене поведінку , коли це не .

Наявність загального базового типу для речей дає змогу перевірити касти серед похідних цього базового типу, а також виконувати спробні операції, але перевірка ролях коштує дорожче, ніж просто припускати, що вони законні і сподіваючись, що нічого поганого не відбудеться. Філософія C ++ полягає в тому, що така перевірка вимагає "оплати за те, що вам, як правило, не потрібно".

Ще одна проблема, яка стосується C ++, але не буде проблемою для нової мови, полягає в тому, що якщо кілька програмістів створюють загальну базу, отримують з цього свої власні класи та пишуть код для роботи з речами цього загального базового класу, такий код не зможе працювати з об’єктами, розробленими програмістами, які використовували інший базовий клас. Якщо нова мова вимагає, щоб усі об’єкти купи мали загальний формат заголовка, і ніколи не дозволяли купувати об’єкти, які не відбулися, то метод, який вимагає посилання на об'єкт купи з таким заголовком, приймає посилання на будь-який об’єкт купи міг коли-небудь створити.

Особисто я вважаю, що наявність загального засобу запитання об’єкта "ти конвертована у тип X" є дуже важливою особливістю мови / рамки, але якщо така функція не вбудована в мову з самого початку, важко додайте його пізніше. Особисто я вважаю, що такий базовий клас повинен бути доданий до стандартної бібліотеки при першій же нагоді з настійною рекомендацією, що всі об'єкти, які будуть використовуватися поліморфно, повинні успадковувати цю базу. Якщо кожен програміст реалізує свої власні "базові типи", це ускладнить передачу об'єктів між кодами різних людей, але мати спільний базовий тип, від якого багато успадкованих програмістів, полегшить його.

ДОБАВЛЕННЯ

Використовуючи шаблони, можна визначити "довільний власник об'єкта" і запитати його про тип об'єкта, що міститься в ньому; пакет Boost містить таке, що називається any. Таким чином, незважаючи на те, що C ++ не має стандартного типу "тип, який можна перевіряти на будь-що", його можна створити. Це не вирішує формульовану проблему з відсутністю чогось мовного стандарту, тобто несумісності між реалізаціями різних програмістів, але це пояснює, як C ++ виходить, не маючи базового типу, з якого все походить: шляхом створення можливості щось, що діє як одне.


Цей викид не вдається під час компіляції в C ++ , Java та C # .
тисячоліття

1
@milleniumbug: Якщо Wooferце інтерфейс і Catє спадковим, він може бути законним, тому що він може існувати (якщо не зараз, можливо, у майбутньому) a, WoofingCatякий успадковується від Catта реалізується Woofer. Зауважте, що за моделлю компіляції / зв’язування Java для створення WoofingCatатрибута не вимагатиме доступу до вихідного коду для Catnor Woofer.
supercat

3
У C ++ є динамічний_cast , який належним чином обробляє спроби передачі від a Catдо a Wooferі відповість на питання "ви конвертовані у тип X". C ++ дозволить вам змусити виступити, викликати ей, можливо, ви насправді знаєте, що робите, але це також допоможе вам, якщо це не те, що ви насправді хочете робити.
Роб К

2
@RobK: Ти маєш рацію щодо синтаксису, звичайно; mea culpa. Я читав трохи більше про dynamic_cast і, здається, у певному сенсі сучасний C ++ має всі поліморфні об'єкти, що походять від базового базового класу "поліморфний об'єкт" з будь-якими полями, необхідними для ідентифікації типу об'єкта (як правило, vtable покажчик, хоча це деталь реалізації). C ++ не описує таким чином поліморфні класи, але передача вказівника на dynamic_castбуде визначати поведінку, якщо вона вказує на поліморфний об’єкт, і Undefined поведінка, якщо цього не відбувається, тож з семантичної точки зору ...
supercat

2
... всі поліморфні об'єкти зберігають деяку інформацію з однаковим розташуванням, і всі підтримують поведінку, яка не підтримується неполіморфними об'єктами; на мій погляд, це означає, що вони поводяться так, ніби вони походять із загальної бази, незалежно від того, використовує мовне визначення таку термінологію чи ні.
supercat

1

Насправді Symbian C ++ мав універсальний базовий клас, CBase, для всіх об'єктів, які поводилися певним чином (головним чином, якщо вони виділяли купу). Він надав віртуальний деструктор, занулював пам'ять класу на побудову і приховав конструктор копій.

Обґрунтування цього полягало в тому, що це мова для вбудованих систем, а компілятори та специфікації на C ++ були справді справжнісінькі лайно 10 років тому.

Не всі класи успадкували від цього, лише деякі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.