Чому в C ++ немає базового класу?


84

Швидке запитання: з точки зору дизайну, чому в C ++ немає базового класу, який зазвичай є objectіншими мовами?


27
Це насправді більше філософське питання, ніж питання дизайну. Коротка відповідь: "тому що Б'ярн, очевидно, не хотів цього робити".
Джеррі Коффін,

4
Також ніщо не заважає вам зробити це.
GWW

22
Особисто я вважаю недоліком дизайну мати все, що походить від одного базового класу. Це спонукає до стилю програмування, який викликає більше проблем, ніж варто (оскільки, як правило, у вас є загальні контейнери об’єктів, від яких вам потім потрібно буде застосовувати фактичний об’єкт (це я нахмурююсь, вважаючи поганим дизайном IMHO)). Чи справді я хочу контейнер, в якому можна розмістити як машини, так і об’єкти автоматизації команд?
Мартін Йорк,

3
@Martin, але подивіться на це так: наприклад, до автоматичного C ++ 0x, вам доводилося використовувати визначенні типу милі для ітераторів або одноразові typedefs. Маючи більш загальну ієрархію класів, ви можете просто використовувати objectабо iterator.
slezica

9
@Santiago: Використання уніфікованої системи типів майже завжди означає, що у вас виходить багато коду, який покладається на поліморфізм, динамічну диспетчеризацію та RTTI, які є відносно дорогими і всі гальмують оптимізацію, яку можливо, коли вони не використовуються.
James McNellis

Відповіді:


116

Остаточне рішення міститься в поширених запитаннях про Stroustrup . Коротше кажучи, це не передає жодного семантичного значення. Це матиме вартість. Шаблони корисніші для контейнерів.

Чому C ++ не має універсального класу Object?

  • Він нам не потрібен: загальне програмування в більшості випадків забезпечує безпечні альтернативи для статичного типу. Інші справи розглядаються з використанням багаторазового успадкування.

  • Немає корисного універсального класу: справді універсальний не має власної семантики.

  • "Універсальний" клас заохочує неакуратно думати про типи та інтерфейси та призводить до перевірки надлишку часу роботи.

  • Використання універсального базового класу передбачає витрати: об’єкти повинні бути розподілені за купою, щоб бути поліморфними; що передбачає вартість пам'яті та доступу. Об’єкти купи не підтримують, природно, семантику копіювання. Об’єкти купи не підтримують просту поведінку з масштабом (що ускладнює управління ресурсами). Універсальний базовий клас заохочує використовувати Dynamiccast та інші перевірки часу виконання.


83
Нам не потрібен ніякий / об'єкт базового класу / нам не потрібен ніякий / контроль думок ...;)
Пісквор залишив будівлю

3
@Piskvor - LOL Я хочу подивитися музичний кліп!
Байрон Уітлок,

10
Objects must be heap-allocated to be polymorphic- Я не вважаю це твердження загалом правильним. Ви безумовно можете створити екземпляр поліморфного класу в стеку і передати його як вказівник на один із його базових класів, даючи поліморфну ​​поведінку.
Байрон,

5
У Java, намагаючись передати посилання- Fooна-посилання-на- Barколи Barне успадковує Foo, детерміновано видає виняток. У C ++ спроба перекинути вказівник на Foo у вказівник на Bar може послати робота назад у часі, щоб вбити Сару Коннор.
supercat

3
@supercat Ось чому ми використовуємо dynamic_cast <>. Щоб уникнути SkyNet і таємниче з'являються дивани Честерфілд.
Капітан Жираф

44

Давайте спочатку подумаємо, чому б вам спочатку хотілося мати базовий клас. Я можу придумати кілька різних причин:

  1. Для підтримки загальних операцій або колекцій, які працюватимуть на об’єктах будь-якого типу.
  2. Включити різні процедури, загальні для всіх об’єктів (наприклад, управління пам’яттю).
  3. Все є об’єктом (без примітивів!). У деяких мовах (наприклад, Objective-C) цього немає, що робить речі досить безладними.

Це дві вагомі причини того, що мови торгових марок Smalltalk, Ruby та Objective-C мають базові класи (технічно, Objective-C насправді не має базового класу, але, незважаючи на всі наміри та цілі, він має).

Для №1 необхідність базового класу, який об’єднує всі об’єкти під єдиним інтерфейсом, усувається включенням шаблонів у С ++. Наприклад:

void somethingGeneric(Base);

Derived object;
somethingGeneric(object);

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

template <class T>
void somethingGeneric(T);

Derived object;
somethingGeneric(object);

Для №2, тоді як у Objective-C процедури управління пам’яттю є частиною реалізації класу і успадковуються від базового класу, управління пам’яттю в C ++ виконується з використанням композиції, а не успадкування. Наприклад, ви можете визначити розумну обертку покажчика, яка буде виконувати підрахунок посилань на об'єкти будь-якого типу:

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

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

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

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


Нічого страшного! Радий, що ти знайшов це корисним :)
Джонатан Стерлінг

2
Для пункту 3 я протиставляю C #. У C # є примітиви, в які можна вставити object( System.Object), але їх не потрібно. Для того, щоб компілятор, intі System.Int32є псевдонімами і можуть бути використані як взаємозамінні; час виконання обробляє бокс, коли це потрібно.
Коул Джонсон,

У C ++ немає потреби в боксі. За допомогою шаблонів компілятор генерує правильний код під час компіляції.
Роб К

2
Зверніть увагу, що, хоча ця (стара) відповідь демонструє розумний вказівник з урахуванням посилань, зараз у C ++ 11 є такий, std::shared_ptrякий слід використовувати замість цього.

15

Домінуючою парадигмою для змінних C ++ є передача за значенням, а не передача за посиланням. Якщо Objectзмусити все виводитись із кореневої системи , передача їх за значенням призведе до помилки ipse facto.

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

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


1
Це не буде помилкою самостійно. Ієрархії типів уже існують, і вони досить добре використовують передане значення, коли це доречно. Однак це заохочувало б більше стратегій, заснованих на купі, де передача-посилання є більш доцільною.
Dennis Zickefoose

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

6

Проблема в тому, що в С ++ є такий тип! Це так void. :-) Будь-який покажчик може бути безпечно неявно переданий void *, включаючи вказівники на основні типи, класи без віртуальної таблиці та класи з віртуальною таблицею.

Оскільки він повинен бути сумісним з усіма цими категоріями об'єктів, voidсам по собі не може містити віртуальних методів. Без віртуальних функцій та RTTI жодна корисна інформація про тип не може бути отримана void(вона відповідає КОЖНОМУ типу, тому може розказувати лише те, що відповідає КОЖНОМУ типу), але віртуальні функції та RTTI роблять прості типи дуже неефективними та зупиняють роботу C ++ мова, придатна для програмування на низькому рівні з прямим доступом до пам'яті тощо.

Отже, існує такий тип. Він просто забезпечує дуже мінімалістичний (по суті, порожній) інтерфейс через низькорівневу природу мови. :-)


Типи покажчиків та типи об’єктів не однакові. Ви не можете мати об’єкт типу void.
Кевін Панько

1
Насправді саме так зазвичай працює успадкування в C ++. Однією з найважливіших речей, які він робить, є сумісність типів покажчика та посилань: там, де потрібен вказівник на клас, може бути вказаний вказівник на похідний клас. А здатність статичного вказівника покажчиків між двома типами є ознакою їхнього з'єднання в ієрархії. З цієї точки зору void, безумовно, є універсальним базовим класом в C ++.
Ellioh

Якщо ви оголосите змінну типу, voidвона не буде скомпільована, і ви отримаєте цю помилку . У Java ви можете мати змінну типу, Objectі це буде працювати. У цьому різниця між void"реальним" типом. Можливо, це правда, що voidце базовий тип для всього, але оскільки він не містить ніяких конструкторів, методів чи полів, немає можливості сказати, чи є він там чи ні. Це твердження неможливо довести або спростувати.
Kevin Panko

1
У Java кожна змінна є посиланням. У цьому різниця. :-) (До речі, в C ++ також є абстрактні класи, які ви не можете використовувати для оголошення змінної). І у своїй відповіді я пояснив, чому неможливо отримати RTTI з порожнечі. Отже, теоретично порожнеча все ще є базовим класом для всього. static_cast є доказом, оскільки він виконує приведення лише між пов'язаними типами, і його можна використовувати для void. Але ви маєте рацію, відсутність доступу RTTI в void дійсно сильно відрізняється від базових типів мов вищого рівня.
Елліо

1
@Ellioh Після консультації зі стандартом C ++ 11 §3.9.1p9, я визнаю, що voidце тип (і виявив дещо розумне використання? : ), але до універсального базового класу все ще далеко. З одного боку, це "неповний тип, який неможливо заповнити", а з іншого - void&тип не існує .
bcrist

-2

C ++ - це сильно набрана мова. Тим не менш, дивно, що він не має універсального типу об’єкта в контексті спеціалізації шаблонів.

Візьмемо, наприклад, шаблон

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

який можна створити як

Hook<decltype(some_function)> ...;

Тепер припустимо, що ми хочемо те саме для певної функції. Люблю

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

зі спеціалізованим екземпляром

Hook<some_function> ...

Але на жаль, навіть незважаючи на те, що клас T може стояти до будь-якого типу (класу чи ні) перед спеціалізацією, не існує еквівалента auto fallback(я використовую цей синтаксис як найбільш очевидний загальний нетиповий у цьому контексті), який міг би стояти для будь-якого нетиповий аргумент шаблону перед спеціалізацією.

Отже, загалом цей шаблон не переходить з аргументів шаблону типу на нетипові аргументи шаблону.

Як і у багатьох куточках мови С ++, відповідь, швидше за все, "жоден член комітету про це не думав".


Навіть якби (щось на зразок) ваш ReturnType fallback(ArgTypes...)працював, це був би поганий дизайн. template <class T, auto fallback> class Hook; template <class ReturnType, class ... ArgTypes> class Hook<ReturnType (ArgTypes...), ReturnType(*fallback)(ArgTypes...)> ...робить те , що ви хочете , і означає , що параметри шаблону з Hookодноманітно видів
Caleth

-4

C ++ спочатку називався "C з класами". Це прогресія мови С, на відміну від деяких інших більш сучасних речей, таких як С #. І ви не можете розглядати C ++ як мову, а як основу мов (так, я пам'ятаю книгу Скотта Мейерса "Ефективний C ++").

C сам по собі є поєднанням мов, мови програмування C та його препроцесором.

С ++ додає ще один мікс:

  • підхід класу / об'єктів

  • шаблони

  • STL

Мені особисто не подобаються деякі речі, які надходять безпосередньо з C на C ++. Одним із прикладів є функція перерахування. Спосіб, яким C # дозволяє розробнику використовувати його, набагато кращий: він обмежує перерахування у власному обсязі, має властивість Count і легко піддається ітерації.

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


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

@Tony: Цікаво, що книга, про яку я навіть не згадував як іншу мову, - це препроцесор, який єдиний ви враховуєте окремо. Я розумію вашу точку зору, оскільки препроцесор спочатку аналізує власні дані. Але наявність шаблонного механізму - це все одно, що мати іншу мову, яка робить узагальнення за вас. Ви застосовуєте синтаксис шаблону, і у вас буде компілятор, що генерує функції для кожного вашого типу. Коли ви можете написати програму, написану на мові C, і подати її вхід до компілятора C ++, це дві мови в одній: C і C з об'єктами => C ++
сергіол

STL, це можна розглядати як доповнення, але воно має дуже практичні заняття та контейнери, які завдяки шаблонам РОЗШИРЕНО розширюють потужність C ++. І переліки, про які я говорив, - РОДНІ переліки. Коли ви говорите про Boost, ви покладаєтесь на сторонній код, який не є РОДНИМ для мови. У C # мені не потрібно включати що-небудь зовнішнє за наявність ітеративного та самостійного переліку. У С ++, фокус, який я часто використовую, полягає у виклику останнього елемента переліку - без присвоєних значень, тому вони автоматично запускаються з нуля та збільшуються - щось на зразок NUM_ITEMS, і це дає мені верхню межу.
sergiol

2
дякую за ваше подальше пояснення - я принаймні бачу, звідки ви родом; Я чув, як кілька людей скаржаться, що навчитися користуватися шаблонами відчувало невідповідність від інших програм програмування на С ++, хоча це, звичайно, не мій досвід. Я залишу інших читачів висловлювати свої думки щодо інших перспектив, які ви представляєте. Ура.
Тоні Делрой,

1
Я думаю, що найбільшою проблемою наявності універсального базового типу є те, що такий тип був би марним, якби не мав жодних віртуальних методів; С ++ не хотів додавати жодних накладних витрат для типів структур, які не мають віртуальних методів, але кожен об'єкт будь-якого типу, що має будь-які віртуальні методи, повинен мати, як мінімум, вказівник на таблицю відправлення. Якби класи та структури населяли різні всесвіти, можливо, можна було б мати універсальний об'єкт класу, але класи та структури - це, по суті, одне і те ж у C ++.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.