Страуструп говорит: «Не изобретайте сразу уникальную базу для всех ваших классов (класс Object). Как правило, вы можете сделать это лучше для многих / большинства классов». (Язык программирования C ++, четвертое издание, раздел 1.3.4)
Почему базовый класс для всех вообще плохая идея, и когда имеет смысл создавать ее?
c++
object-oriented
object-oriented-design
Мэтью Джеймс Бриггс
источник
источник
Ответы:
Потому что что бы этот объект имел для функциональности? В Java все базовый класс имеет toString, hashCode & равенства и монитор + переменную условия.
ToString полезен только для отладки.
hashCode полезен только в том случае, если вы хотите сохранить его в коллекции на основе хеша (предпочтение в C ++ состоит в том, чтобы передать функцию хеширования в контейнер в качестве параметра шаблона или
std::unordered_*
вообще избежать , а вместо этого использоватьstd::vector
и отображать неупорядоченные списки).равенство без базового объекта может помочь во время компиляции, если они не имеют одинаковый тип, то они не могут быть равны. В C ++ это ошибка времени компиляции.
переменная монитора и условия лучше включать явно в каждом конкретном случае.
Однако, когда нужно сделать больше, тогда есть вариант использования.
Например, в QT есть корневой
QObject
класс, который формирует основу схожести потоков, иерархии родительских и дочерних владений и механизма сигнальных слотов. Это также вынуждает использовать указатель для QObjects, однако многие классы в Qt не наследуют QObject, потому что им не нужен сигнальный слот (особенно типы значений некоторого описания).источник
Object
._hashCode
- это не «использовать другой контейнер», а указание из того, что C ++std::unordered_map
выполняет хеширование с использованием аргумента шаблона, вместо того, чтобы требовать сам класс элемента для обеспечения реализации. То есть, как и все другие хорошие контейнеры и менеджеры ресурсов в C ++, это не навязчиво; он не загрязняет все объекты функциями или данными на тот случай, если кому-то они понадобятся позже.Потому что нет функций, общих для всех объектов. В этом интерфейсе нет ничего, что имело бы смысл для всех классов.
источник
Всякий раз, когда вы строите высокие иерархии наследования объектов, вы склонны сталкиваться с проблемой хрупкого базового класса (Википедия) .
Наличие множества небольших отдельных (отдельных, изолированных) иерархий наследования снижает шансы столкнуться с этой проблемой.
Включение всех ваших объектов в единую иерархию наследования практически гарантирует, что вы столкнетесь с этой проблемой.
источник
cout.print(x).print(0.5).print("Bye\n")
- это не зависитoperator<<
.Потому что:
Реализация любого вида
virtual
функции вводит виртуальную таблицу, которая требует затрат на пространство для каждого объекта, что не является ни необходимым, ни желательным во многих (большинстве?) Ситуациях.Реализация не
toString
виртуально была бы довольно бесполезной, потому что единственное, что он мог бы вернуть, - это адрес объекта, который очень недружелюбен к пользователю и к которому у вызывающей стороны уже есть доступ, в отличие от Java.Точно так же не виртуальный
equals
илиhashCode
может использовать только адреса для сравнения объектов, что опять-таки довольно бесполезно и часто даже совершенно неправильно - в отличие от Java, объекты часто копируются в C ++, и, следовательно, различать «идентичность» объекта даже не всегда значимым или полезным. (Например, уint
действительно не должно быть идентификатора кроме его значения ... два целых числа одного и того же значения должны быть равны.)источник
open
ключевое слово. Я не думаю, что это выходило за рамки нескольких газет, хотя.shared_ptr<Foo>
чтобы увидеть, является ли он такжеshared_ptr<Bar>
(или аналогично другим типам указателей), даже еслиFoo
иBar
являются несвязанными классами, которые ничего не знают друг о друге. Требование, чтобы такая вещь работала с «необработанными указателями», учитывая историю того, как такие вещи используются, было бы дорого, но для вещей, которые все равно будут храниться в куче, добавленная стоимость будет минимальной.Наличие одного корневого объекта ограничивает то, что вы можете делать и что может делать компилятор, без особой отдачи.
Общий корневой класс позволяет создавать контейнеры чего угодно и извлекать из них то, что они есть
dynamic_cast
, но если вам нужны контейнеры чего угодно, то что-то похожееboost::any
может сделать это без общего корневого класса. Аboost::any
также поддерживает примитивы - он может даже поддерживать оптимизацию небольших буферов и оставлять их почти «без коробки» на языке Java.C ++ поддерживает и процветает на типах значений. И литералы, и программисты пишут типы значений. Контейнеры C ++ эффективно хранят, сортируют, хэшируют, потребляют и генерируют типы значений.
Наследование, особенно тот тип монолитного наследования, который подразумевают базовые классы в стиле Java, требует наличия «указателя» или «справочных» типов в хранилище. Ваш дескриптор / указатель / ссылка на данные содержит указатель на интерфейс класса и полиморфно может представлять что-то еще.
Хотя это полезно в некоторых ситуациях, после того, как вы вышли замуж за шаблон с «общим базовым классом», вы привязали всю свою кодовую базу к стоимости и багажу этого шаблона, даже если он бесполезен.
Почти всегда вы знаете больше о типе, чем "это объект", либо на вызывающем сайте, либо в коде, который его использует.
Если функция проста, то написание функции в виде шаблона дает вам полиморфизм времени компиляции типа утки, при котором информация на вызывающем сайте не выбрасывается. Если функция более сложная, стирание типа может быть выполнено, в результате чего единообразные операции над типом, который вы хотите выполнить (скажем, сериализация и десериализация), могут быть построены и сохранены (во время компиляции) для использования (во время выполнения) код в другой единице перевода.
Предположим, у вас есть библиотека, где вы хотите, чтобы все было сериализуемо. Один из подходов - иметь базовый класс:
Теперь каждый бит кода, который вы пишете, может быть
serialization_friendly
.За исключением не
std::vector
, так что теперь вам нужно написать каждый контейнер. И не те целые числа, которые вы получили из этой библиотеки bignum. И не тот тип, который вы написали, что вы не нуждались в сериализации. И не atuple
,int
или adouble
, или a, или astd::ptrdiff_t
.Мы используем другой подход:
который состоит из, ну, ничего не делать, по-видимому. За исключением того, что теперь мы можем расширять
write_to
, переопределяяwrite_to
как свободную функцию в пространстве имен типа или метода в типе.Мы можем даже написать немного кода стирания типа:
и теперь мы можем взять произвольный тип и автоматически поместить его в
can_serialize
интерфейс, который позволит вам вызыватьserialize
его позже через виртуальный интерфейс.Так:
это функция, которая принимает все, что может сериализовать, вместо
и первое, в отличие от вторых, он может работать
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 ++ является достаточно мощным, чтобы вы могли реализовать преимущества одного базового класса на лету, когда это необходимо, без необходимости расплачиваться за иерархию принудительного наследования, когда она не требуется. И времена, когда требуется одна база (поддельная или нет), достаточно редки.
Когда типы фактически являются их идентичностью, и вы знаете, что они есть, возможностей для оптимизации предостаточно. Данные хранятся локально и непрерывно (что очень важно для удобства кэширования на современных процессорах), компиляторы могут легко понять, что делает данная операция (вместо того, чтобы иметь непрозрачный указатель виртуального метода, который она должна перепрыгивать, приводя к неизвестному коду на с другой стороны), что позволяет оптимально переупорядочивать инструкции, и меньшее количество круглых колышков забивается в круглые отверстия.
источник
Выше приведено много хороших ответов, и очевидный факт, что все, что вы будете делать с базовым классом всех объектов, можно сделать лучше другими способами, как показано в ответе @ ratchetfreak, и комментарии к нему очень важны, но Существует еще одна причина, которая заключается в том, чтобы избежать создания наследства алмазовкогда используется множественное наследование. Если у вас есть какие-либо функциональные возможности в универсальном базовом классе, как только вы начнете использовать множественное наследование, вам придется начинать указывать, к какому варианту его нужно обратиться, поскольку он может быть перегружен по-разному в разных путях цепочки наследования. И база не может быть виртуальной, потому что это будет очень неэффективно (требуется, чтобы у всех объектов была виртуальная таблица с потенциально огромными затратами на использование памяти и локальность). Это станет логистическим кошмаром очень быстро.
источник
На самом деле Microsoft ранних компиляторов и библиотек C ++ (я знаю о Visual C ++, 16 бит) имел такой класс под названием
CObject
.Однако вы должны знать, что в то время этот простой компилятор C ++ не поддерживал «шаблоны», поэтому такие классы
std::vector<class T>
были невозможны. Вместо этого «векторная» реализация может обрабатывать только один тип класса, поэтому существует класс, сопоставимый сstd::vector<CObject>
сегодняшним днем. ПосколькуCObject
был базовый класс почти всех классов ( к сожалению , неCString
- эквивалентstring
в современных компиляторов) , вы могли бы использовать этот класс для хранения почти всех видов объектов.Поскольку современные компиляторы поддерживают шаблоны, этот вариант использования «базового класса» больше не приводится.
Вы должны подумать о том, что использование такого базового базового класса будет стоить (немного) памяти и времени выполнения - например, при вызове конструктора. Таким образом, при использовании такого класса есть недостатки, но, по крайней мере, при использовании современных компиляторов C ++ для такого класса практически нет вариантов использования.
источник
TObject
до появления MFC. Не обвиняйте Microsoft в этой части дизайна, это казалось хорошей идеей для всех в то время.Я собираюсь предложить другую причину, которая исходит от Java.
Потому что вы не можете создать базовый класс для всего, по крайней мере, без связки.
Возможно, вам удастся сойти с рук для ваших собственных классов - но вы, вероятно, обнаружите, что в конечном итоге вы дублируете много кода. Например, «я не могу использовать
std::vector
здесь, так как он не реализуетсяIObject
- я бы лучше создал новый производный,IVectorObject
который делает правильные вещи ...».Это будет иметь место всякий раз, когда вы имеете дело со встроенными или стандартными библиотеками классов или классов из других библиотек.
Теперь, если бы он был встроен в язык, вы бы столкнулись с такими вещами, как
Integer
иint
путаница в Java, или с большим изменением синтаксиса языка. (Имейте в виду, я думаю, что некоторые другие языки проделали хорошую работу, встроив его в каждый тип - ruby кажется лучшим примером.)Также обратите внимание, что если ваш базовый класс не является полиморфным во время выполнения (то есть с использованием виртуальных функций), вы можете получить то же преимущество от использования таких черт, как framework.
например, вместо
.toString()
вас может быть следующее: (ПРИМЕЧАНИЕ: я знаю, что вы можете сделать это аккуратно, используя существующие библиотеки и т. д., это просто иллюстративный пример.)источник
Возможно, «пустота» выполняет много ролей универсального базового класса. Вы можете привести любой указатель к
void*
. Затем вы можете сравнить эти указатели. Вы можетеstatic_cast
вернуться к исходному классу.Однако то, что вы не можете сделать с помощью
void
чего вы можете сделать,Object
это использовать RTTI, чтобы выяснить, какой тип объекта у вас действительно есть. В конечном счете, это связано с тем, что не все объекты в C ++ имеют RTTI, и действительно возможно иметь объекты нулевой ширины.источник
[[no_unique_address]]
, который может использоваться компиляторами для придания подобъектам нулевой ширины.[[no_unique_address]]
позволит компилятору переменные-члены EBO.Java берет философию дизайна, что Неопределенное Поведение не должно существовать . Код такой как:
проверит,
felix
содержит ли подтипCat
этого реализуемого интерфейсаWoofer
; если это произойдет, он выполнит приведение и вызовет,woof()
а если нет, то выдаст исключение. Поведение кода полностью определено,felix
реализует онWoofer
или нет .C ++ придерживается философии, что если программа не должна пытаться выполнить какую-либо операцию, не должно иметь значения, что сгенерированный код будет делать, если будет предпринята эта операция, и компьютер не должен тратить время, пытаясь ограничить поведение в случаях, которые «должны» никогда не возникает. В C ++, добавляя соответствующие операторы косвенности, чтобы привести a
*Cat
к a*Woofer
, код выдает определенное поведение, когда приведение является допустимым, но неопределенное поведение, когда это не так .Наличие общего базового типа для вещей позволяет проверять приведение среди производных этого базового типа, а также выполнять операции try-cast, но проверка приведений обходится дороже, чем просто предполагать, что они законны, и надеяться, что ничего плохого не произойдет. Философия C ++ заключается в том, что такая проверка требует «платить за то, что вам [обычно] не нужно».
Другая проблема, которая относится к C ++, но не будет проблемой для нового языка, заключается в том, что если несколько программистов каждый создают общую базу, выводят из нее свои собственные классы и пишут код для работы с вещами этого общего базового класса, такой код не сможет работать с объектами, разработанными программистами, которые использовали другой базовый класс. Если новый язык требует, чтобы все объекты кучи имели общий формат заголовка, и никогда не разрешал объекты кучи, которые этого не делали, то метод, который требует ссылку на объект кучи с таким заголовком, примет ссылку на любой объект кучи любого мог когда-либо создать.
Лично я считаю, что наличие общего способа задать объекту «вы конвертируемы в тип X» - очень важная функция в языке / среде, но если такая функция не встроена в язык с самого начала, трудно добавь это позже. Лично я считаю, что такой базовый класс должен быть добавлен в стандартную библиотеку при первой возможности с настоятельной рекомендацией, что все объекты, которые будут использоваться полиморфно, должны наследоваться от этой базы. Если бы каждый программист реализовывал свои собственные «базовые типы», это усложняло бы передачу объектов между кодами разных людей, но наличие общего базового типа, от которого унаследовали многие программисты, было бы проще.
ДОПОЛНЕНИЕ
Используя шаблоны, можно определить «произвольного держателя объекта» и спросить его о типе объекта, содержащегося в нем; пакет Boost содержит такую вещь, как
any
. Таким образом, даже несмотря на то, что C ++ не имеет стандартного типа «проверяемая на тип ссылка на что-либо», его можно создать. Это не решает вышеупомянутую проблему с отсутствием чего-либо в стандарте языка, то есть несовместимость между реализациями разных программистов, но это объясняет, как C ++ обходится без базового типа, из которого все происходит: путем создания возможности создания что-то, что действует как один.источник
Woofer
интерфейс иCat
наследуется, приведение будет законным, потому что может существовать (если не сейчас, возможно, в будущем) объект,WoofingCat
который наследуетCat
и реализуетWoofer
. Обратите внимание, что в модели компиляции / компоновки Java создание aWoofingCat
не требует доступа к исходному коду дляCat
norWoofer
.Cat
в aWoofer
и отвечает на вопрос «вы конвертируемы в тип X». C ++ позволит вам вызвать приведение, потому что, может быть, вы действительно знаете, что делаете, но это также поможет вам, если это не то, что вы действительно хотите делать.dynamic_cast
будет иметь определенное поведение, если он указывает на полиморфный объект, и неопределенное поведение, если это не так, с семантической точки зрения ...Symbian C ++ действительно имел универсальный базовый класс CBase для всех объектов, которые ведут себя определенным образом (в основном, если они выделяют кучу). Он предоставлял виртуальный деструктор, обнулял память класса при создании и скрывал конструктор копирования.
Основанием для этого было то, что это был язык для встроенных систем, а компиляторы C ++ и спецификации действительно были дерьмовыми 10 лет назад.
Не все классы унаследованы от этого, только некоторые.
источник