Почему базовые для всех объектов не рекомендуется в C ++

76

Страуструп говорит: «Не изобретайте сразу уникальную базу для всех ваших классов (класс Object). Как правило, вы можете сделать это лучше для многих / большинства классов». (Язык программирования C ++, четвертое издание, раздел 1.3.4)

Почему базовый класс для всех вообще плохая идея, и когда имеет смысл создавать ее?

Мэтью Джеймс Бриггс
источник
16
потому что C ++ не Java ... И вы не должны пытаться заставить его быть.
AK_
10
На вопрос о переполнении стека: почему в C ++ нет базового класса?
26
Кроме того, я не согласен с голосами за «в первую очередь на основе мнений». Для этого можно объяснить очень конкретные причины, поскольку ответы свидетельствуют как на этот вопрос, так и на связанный вопрос SO.
2
Это принцип гибкости «тебе это не понадобится». Если вы уже не определили особую потребность в этом, не делайте этого (пока не сделаете).
Jool
3
@AK_: в твоем комментарии отсутствует слово "глупый как".
DeadMG

Ответы:

75

Потому что что бы этот объект имел для функциональности? В Java все базовый класс имеет toString, hashCode & равенства и монитор + переменную условия.

  • 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 :: vector и простых неупорядоченных списков). « Реальный контраргумент _hashCode- это не «использовать другой контейнер», а указание из того, что C ++ std::unordered_mapвыполняет хеширование с использованием аргумента шаблона, вместо того, чтобы требовать сам класс элемента для обеспечения реализации. То есть, как и все другие хорошие контейнеры и менеджеры ресурсов в C ++, это не навязчиво; он не загрязняет все объекты функциями или данными на тот случай, если кому-то они понадобятся позже.
underscore_d
100

Потому что нет функций, общих для всех объектов. В этом интерфейсе нет ничего, что имело бы смысл для всех классов.

DeadMG
источник
10
+1 для простоты ответа, это действительно единственная причина.
BWG
7
В большой структуре, с которой у меня есть опыт работы, общий базовый класс обеспечивает инфраструктуру сериализации и отражения, которая желательна в контексте <что угодно>. Мех. Это просто привело к тому, что люди сериализовали кучу беспорядка вместе с данными и метаданными, и сделали формат данных слишком большим и сложным, чтобы быть эффективным.
dmckee
19
@dmckee: Я бы также сказал, что сериализация и рефлексия вряд ли являются универсально полезными потребностями.
DeadMG
16
@DeadMG: «НО ЧТО, ЕСЛИ ВАМ НУЖНО СОХРАНИТЬ ВСЕ?»
deworde
8
Я не знаю, ты вставляешь это в кавычки, ты используешь все заглавные буквы, и люди не могут видеть шутку. @MSalters: Ну, это должно быть легко, у него минимальное количество состояний, вы просто указываете, что оно есть. Я могу написать свое имя в списке, не входя в рекурсивный цикл.
deworde
25

Всякий раз, когда вы строите высокие иерархии наследования объектов, вы склонны сталкиваться с проблемой хрупкого базового класса (Википедия) .

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

Включение всех ваших объектов в единую иерархию наследования практически гарантирует, что вы столкнетесь с этой проблемой.

Майк Накис
источник
6
Когда базовый класс (в Java «java.lang.Object») не содержит методов, вызывающих другие методы, проблема Fragile Base Class не может возникнуть.
Мартин Розенау
3
Могучий полезный базовый класс, который был бы!
Майк Накис
9
@MartinRosenau ... как вы можете сделать в C ++, не требуя мастер-класса!
gbjbaanb
5
@ DavorŽdralo Итак, в C ++ есть глупое имя для базовой функции («operator <<» вместо чего-то разумного, например, «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действительно не должно быть идентификатора кроме его значения ... два целых числа одного и того же значения должны быть равны.)

Mehrdad
источник
В связи с этой проблемой и проблемой хрупкого базового класса, отмеченной Майком Накисом, обратите внимание на интересное исследование / предложение по исправлению его в Java в основном за счет того, что все методы внутренне (т.е. при вызове из одного и того же класса) не являются виртуальными, но сохраняют свое виртуальное поведение, когда внешне называется; чтобы получить старое / стандартное поведение (то есть виртуальное везде), в предложении было введено новое openключевое слово. Я не думаю, что это выходило за рамки нескольких газет, хотя.
Fizz
Немного больше обсуждения об этом документе можно найти по адресу lambda-the-ultimate.org/classic/message12271.html
Fizz,
Наличие общего базового класса позволило бы протестировать любой из них, shared_ptr<Foo> чтобы увидеть, является ли он также shared_ptr<Bar>(или аналогично другим типам указателей), даже если Fooи Barявляются несвязанными классами, которые ничего не знают друг о друге. Требование, чтобы такая вещь работала с «необработанными указателями», учитывая историю того, как такие вещи используются, было бы дорого, но для вещей, которые все равно будут храниться в куче, добавленная стоимость будет минимальной.
суперкат
Хотя может быть не полезно иметь общий базовый класс для всего, я думаю, что есть довольно большие категории объектов, для которых были бы полезны общие базовые классы. Например, многие (существенное множество, если не большинство) классов в Java могут использоваться двумя способами: как неразделенный держатель изменяемых данных или как разделяемый держатель данных, которые никому не разрешено изменять. В обоих шаблонах использования управляемый указатель (ссылка) используется как прокси для базовых данных. Полезно иметь общий тип управляемого указателя для всех таких данных.
суперкат
16

Наличие одного корневого объекта ограничивает то, что вы можете делать и что может делать компилятор, без особой отдачи.

Общий корневой класс позволяет создавать контейнеры чего угодно и извлекать из них то, что они есть dynamic_cast, но если вам нужны контейнеры чего угодно, то что-то похожее boost::anyможет сделать это без общего корневого класса. А boost::anyтакже поддерживает примитивы - он может даже поддерживать оптимизацию небольших буферов и оставлять их почти «без коробки» на языке 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запросов сохраняется в виртуальной таблице и доступна для них .write_to.

Короче говоря, C ++ является достаточно мощным, чтобы вы могли реализовать преимущества одного базового класса на лету, когда это необходимо, без необходимости расплачиваться за иерархию принудительного наследования, когда она не требуется. И времена, когда требуется одна база (поддельная или нет), достаточно редки.

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

Yakk
источник
8

Выше приведено много хороших ответов, и очевидный факт, что все, что вы будете делать с базовым классом всех объектов, можно сделать лучше другими способами, как показано в ответе @ ratchetfreak, и комментарии к нему очень важны, но Существует еще одна причина, которая заключается в том, чтобы избежать создания наследства алмазовкогда используется множественное наследование. Если у вас есть какие-либо функциональные возможности в универсальном базовом классе, как только вы начнете использовать множественное наследование, вам придется начинать указывать, к какому варианту его нужно обратиться, поскольку он может быть перегружен по-разному в разных путях цепочки наследования. И база не может быть виртуальной, потому что это будет очень неэффективно (требуется, чтобы у всех объектов была виртуальная таблица с потенциально огромными затратами на использование памяти и локальность). Это станет логистическим кошмаром очень быстро.

Жюль
источник
1
Одно из решений алмазной проблемы состоит в том, чтобы все типы, которые фактически не выводят базовый тип через несколько путей, переопределяли все виртуальные члены этого базового типа; если общий базовый тип был встроен в язык с самого начала, компилятор может автоматически генерировать законные (хотя и не обязательно впечатляющие) реализации по умолчанию.
суперкат
5

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

Однако вы должны знать, что в то время этот простой компилятор C ++ не поддерживал «шаблоны», поэтому такие классы std::vector<class T>были невозможны. Вместо этого «векторная» реализация может обрабатывать только один тип класса, поэтому существует класс, сопоставимый с std::vector<CObject>сегодняшним днем. Поскольку CObjectбыл базовый класс почти всех классов ( к сожалению , не CString- эквивалент stringв современных компиляторов) , вы могли бы использовать этот класс для хранения почти всех видов объектов.

Поскольку современные компиляторы поддерживают шаблоны, этот вариант использования «базового класса» больше не приводится.

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

Мартин Розенау
источник
3
Это МФЦ? [отступы для комментариев]
user253751
3
Это действительно MFC. Яркий маяк ОО-дизайна, который показал миру, как все должно быть сделано. Ой, подождите ...
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, или с большим изменением синтаксиса языка. (Имейте в виду, я думаю, что некоторые другие языки проделали хорошую работу, встроив его в каждый тип - ruby ​​кажется лучшим примером.)

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

например, вместо .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

Возможно, «пустота» выполняет много ролей универсального базового класса. Вы можете привести любой указатель к void*. Затем вы можете сравнить эти указатели. Вы можете static_castвернуться к исходному классу.

Однако то, что вы не можете сделать с помощью voidчего вы можете сделать, Objectэто использовать RTTI, чтобы выяснить, какой тип объекта у вас действительно есть. В конечном счете, это связано с тем, что не все объекты в C ++ имеют RTTI, и действительно возможно иметь объекты нулевой ширины.

pjc50
источник
1
Только подобъекты базового класса нулевой ширины, а не нормальные.
Дедупликатор
@Deduplicator В качестве обновления добавляется C ++ 17 [[no_unique_address]], который может использоваться компиляторами для придания подобъектам нулевой ширины.
underscore_d
1
@underscore_d Вы имеете в виду запланированный на C ++ 20, [[no_unique_address]]позволит компилятору переменные-члены EBO.
дедупликатор
@ Дедупликатор Ой, ага. Я уже начал использовать C ++ 17, но, думаю, я все еще думаю, что он более современный, чем есть на самом деле!
underscore_d
2

Java берет философию дизайна, что Неопределенное Поведение не должно существовать . Код такой как:

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

проверит, felixсодержит ли подтип Catэтого реализуемого интерфейса Woofer; если это произойдет, он выполнит приведение и вызовет, woof()а если нет, то выдаст исключение. Поведение кода полностью определено, felixреализует он Wooferили нет .

C ++ придерживается философии, что если программа не должна пытаться выполнить какую-либо операцию, не должно иметь значения, что сгенерированный код будет делать, если будет предпринята эта операция, и компьютер не должен тратить время, пытаясь ограничить поведение в случаях, которые «должны» никогда не возникает. В C ++, добавляя соответствующие операторы косвенности, чтобы привести a *Catк a *Woofer, код выдает определенное поведение, когда приведение является допустимым, но неопределенное поведение, когда это не так .

Наличие общего базового типа для вещей позволяет проверять приведение среди производных этого базового типа, а также выполнять операции try-cast, но проверка приведений обходится дороже, чем просто предполагать, что они законны, и надеяться, что ничего плохого не произойдет. Философия C ++ заключается в том, что такая проверка требует «платить за то, что вам [обычно] не нужно».

Другая проблема, которая относится к C ++, но не будет проблемой для нового языка, заключается в том, что если несколько программистов каждый создают общую базу, выводят из нее свои собственные классы и пишут код для работы с вещами этого общего базового класса, такой код не сможет работать с объектами, разработанными программистами, которые использовали другой базовый класс. Если новый язык требует, чтобы все объекты кучи имели общий формат заголовка, и никогда не разрешал объекты кучи, которые этого не делали, то метод, который требует ссылку на объект кучи с таким заголовком, примет ссылку на любой объект кучи любого мог когда-либо создать.

Лично я считаю, что наличие общего способа задать объекту «вы конвертируемы в тип X» - очень важная функция в языке / среде, но если такая функция не встроена в язык с самого начала, трудно добавь это позже. Лично я считаю, что такой базовый класс должен быть добавлен в стандартную библиотеку при первой возможности с настоятельной рекомендацией, что все объекты, которые будут использоваться полиморфно, должны наследоваться от этой базы. Если бы каждый программист реализовывал свои собственные «базовые типы», это усложняло бы передачу объектов между кодами разных людей, но наличие общего базового типа, от которого унаследовали многие программисты, было бы проще.

ДОПОЛНЕНИЕ

Используя шаблоны, можно определить «произвольного держателя объекта» и спросить его о типе объекта, содержащегося в нем; пакет Boost содержит такую ​​вещь, как any. Таким образом, даже несмотря на то, что C ++ не имеет стандартного типа «проверяемая на тип ссылка на что-либо», его можно создать. Это не решает вышеупомянутую проблему с отсутствием чего-либо в стандарте языка, то есть несовместимость между реализациями разных программистов, но это объясняет, как C ++ обходится без базового типа, из которого все происходит: путем создания возможности создания что-то, что действует как один.

Supercat
источник
Это приведение не выполняется во время компиляции в C ++ , Java и C # .
Milleniumbug
1
@milleniumbug: Если Wooferинтерфейс и Catнаследуется, приведение будет законным, потому что может существовать (если не сейчас, возможно, в будущем) объект, WoofingCatкоторый наследует Catи реализует Woofer. Обратите внимание, что в модели компиляции / компоновки Java создание a WoofingCatне требует доступа к исходному коду для Catnor Woofer.
суперкат
3
В C ++ есть dynamic_cast , который правильно обрабатывает попытки приведения из a Catв a Wooferи отвечает на вопрос «вы конвертируемы в тип X». C ++ позволит вам вызвать приведение, потому что, может быть, вы действительно знаете, что делаете, но это также поможет вам, если это не то, что вы действительно хотите делать.
Роб К
2
@RobK: Вы правы насчет синтаксиса, конечно; моя вина. Я читал немного больше о dynamic_cast, и кажется, что в некотором смысле современный C ++ имеет все полиморфные объекты, производные от базового базового класса «полиморфный объект», с любыми полями, необходимыми для идентификации типа объекта (обычно это vtable указатель, хотя это деталь реализации). C ++ не описывает полиморфные классы таким образом, но передача указателя на dynamic_castбудет иметь определенное поведение, если он указывает на полиморфный объект, и неопределенное поведение, если это не так, с семантической точки зрения ...
суперкат
2
... все полиморфные объекты хранят некоторую информацию с одинаковым расположением и поддерживают поведение, которое не поддерживается неполиморфными объектами; На мой взгляд, это означает, что они ведут себя так, как будто они происходят из общей базы, независимо от того, использует ли определение языка такую ​​терминологию или нет.
суперкат
1

Symbian C ++ действительно имел универсальный базовый класс CBase для всех объектов, которые ведут себя определенным образом (в основном, если они выделяют кучу). Он предоставлял виртуальный деструктор, обнулял память класса при создании и скрывал конструктор копирования.

Основанием для этого было то, что это был язык для встроенных систем, а компиляторы C ++ и спецификации действительно были дерьмовыми 10 лет назад.

Не все классы унаследованы от этого, только некоторые.

Джеймс
источник