Подтвердить что ты не робот

Почему бы не использовать все функции как виртуальные в С++?

Я знаю, что виртуальные функции имеют накладные расходы на разыменование, чтобы вызвать метод. Но я думаю, что с современной архитектурой скорость почти ничтожна.

  • Есть ли какая-то особая причина, почему все функции в С++ не являются виртуальными, как в Java?
  • Насколько я знаю, определение функции virtual в базовом классе является достаточным/необходимым. Теперь, когда я пишу родительский класс, я, возможно, не знаю, какие методы будут преодолены. Значит ли это, что при написании дочернего класса кому-то придется редактировать родительский класс. Это звучит как неудобно, а иногда и невозможно?

Update:
Подводя итог от Jon Skeet, ответьте ниже:

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

Другие причины из разных ответов:

Виртуальные функции не могут быть связаны друг с другом, поскольку в процессе выполнения встроенные функции должны выполняться. Это влияет на производительность, когда вы ожидаете, что функции будут полезны при встраивании.

Там могут быть и другие причины, и я хотел бы узнать и обобщить их.

4b9b3361

Ответ 1

Есть веские причины для контроля того, какие методы являются виртуальными за пределами производительности. Хотя я фактически не делаю большинство своих методов окончательными в Java, я, вероятно, должен... если только метод не будет переопределен, вероятно, он не должен быть виртуальным IMO.

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

Обратите внимание, что С# - это похожий язык на Java по-разному, но по умолчанию предпочитают делать не виртуальные методы. Некоторые другие люди не увлекаются этим, но я, конечно же, приветствую это, и я действительно предпочел бы, чтобы классы по умолчанию тоже были неприемлемы.

В принципе, это сводится к этому совету Джоша Блоха: дизайн для наследования или запрет.

Ответ 2

  • Один из основных принципов С++: вы платите только за то, что используете ( "принцип нулевой надбавки" ). Если вам не нужен динамический механизм отправки, вы не должны оплачивать его накладные расходы.

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

Ответ 3

Но я думаю, что с современной архитектурой скорость почти ничтожна.

Это предположение неверно и, я думаю, главная причина этого решения.

Рассмотрим случай вложения. Функция С++ sort выполняет намного быстрее, чем Cs, в противном случае, аналогично qsort в некоторых сценариях, потому что она может встроить аргумент компаратора, а C не может (из-за использования указателей функций). В крайних случаях это может означать разницу в производительности до 700% (Scott Meyers, Effective STL).

То же самое можно сказать и о виртуальных функциях. У нас были подобные дискуссии раньше; например, Есть ли причина использовать С++ вместо C, Perl, Python и т.д.

Ответ 4

Большинство ответов касаются накладных расходов на виртуальные функции, но есть и другие причины не делать какие-либо функции в классе virtual, поскольку тот факт, что он изменит класс от стандартного макета до, ну, нестандартного макета, и это может быть проблемой, если вам нужно сериализовать двоичные данные. Это решается по-другому в С#, например, если struct является другим семейством типов, чем class es.

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

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

Это похоже на пример бесконечного переполнения цикла/стека, упомянутого в @Jon Skeet, по-другому: вы должны документировать в каждой функции, получает ли он какие-либо частные атрибуты, чтобы расширения обеспечивали, чтобы эта функция была в нужное время. А это, в свою очередь, означает, что вы нарушаете инкапсуляцию, и у вас есть абсорбирующая абстракция: ваши внутренние детали теперь являются частью интерфейса (документация + требования к вашим расширениям), и вы не можете изменять их по своему усмотрению.

Тогда есть производительность... будет влияние на производительность, но в большинстве случаев это переоценено, и можно утверждать, что только в тех немногих случаях, когда производительность критическая, вы отступаете и объявляете функции невиртуальном. Опять же, это может быть непростым для встроенного продукта, так как два интерфейса (public + extensions) уже связаны.

Ответ 5

Ты забыл одно. Накладные расходы также находятся в памяти, то есть вы добавляете виртуальную таблицу и указатель на эту таблицу для каждого объекта. Теперь, если у вас есть объект, который имеет значительное количество ожидаемых случаев, это не является незначительным. Например, миллион экземпляров равен 4 мегабайтам. Я согласен с тем, что для простого применения это немного, но для устройств реального времени, таких как маршрутизаторы, это учитывается.

Ответ 7

Я опаздываю на вечеринку здесь, поэтому добавлю одну вещь, которую я не заметил в других ответах, и быстро подытожим...

  • Удобство использования в общей памяти: типичная реализация виртуальной диспетчеризации имеет указатель на таблицу виртуальной диспетчеризации для каждого объекта. Адреса в этих указателях относятся к процессу, создающему их, что означает, что системы с несколькими процессами, обращающиеся к объектам в общей памяти, не могут отправлять с использованием другого объекта процесса! Это неприемлемое ограничение, учитывая важность общей памяти в высокопроизводительных многопроцессорных системах.

  • Инкапсуляция: способность конструктора классов управлять членами, доступными по коду клиента, обеспечивая сохранение семантики класса и инвариантов. Например, если вы выходите из std::string (я могу получить несколько комментариев для смелости предложить это; -P), то вы можете использовать все обычные операции вставки/стирания/добавления и быть уверенными, что - если вы не делаете все, что всегда undefined поведение для std::string, как передача плохих значений позиции для функций - данные std::string будут звуковыми. Кто-то, проверяющий или поддерживающий ваш код, не должен проверять, изменилось ли значение этих операций. Для класса инкапсуляция гарантирует свободу для последующего изменения реализации без нарушения кода клиента. Другая перспектива в том же самом заявлении: клиентский код может использовать класс по своему усмотрению, не будучи чувствительным к деталям реализации. Если какая-либо функция может быть изменена в производном классе, весь механизм инкапсуляции просто сбрасывается.

    • Скрытые зависимости: когда вы не знаете, какие другие функции зависят от того, который вы переопределяете, или что функция была предназначена для переопределения, тогда вы не можете рассуждать о влиянии ваше изменение. Например, вы думаете: "Я всегда этого хотел" и изменяю std::string::operator[]() и at(), чтобы считать отрицательные значения (после того, как тип-приведение к подписанию) будет смещаться назад от конца строки. Но, возможно, какая-то другая функция использовала at() как своего рода утверждение о том, что индекс действителен - зная, что он будет выдавать иначе - перед попыткой вставки или удаления... этот код может перейти от металирования стандартным образом к undefined (но, вероятно, летальному) поведению.
    • Документация: создав функцию virtual, вы документируете, что это предполагаемая точка настройки и часть API для клиентского кода.

  • Встраивание - использование кода и использование ЦП: виртуальная диспетчеризация усложняет работу компилятора, когда вы разрабатываете встроенные вызовы функций, и поэтому может обеспечить худший код с точки зрения как пробела, так и раздутия Использование ЦП.

  • Направление во время вызовов: даже если внеконтактный вызов выполняется в любом случае, существует небольшая стоимость для виртуальной отправки, что может быть значительным при повторном вызове тривиально простых функций в критически важных системах. (Вы должны прочитать указатель на объект для таблицы виртуальной рассылки, а сама запись таблицы виртуальной диспетчеризации - означает, что страницы VDT тоже потребляют кеш.)

  • Использование памяти: указатели на объекты для виртуальных таблиц отправки могут представлять значительную потерянную память, особенно для массивов небольших объектов. Это означает, что меньше объектов помещается в кеш и может иметь значительное влияние на производительность.

  • Макет памяти: он необходим для производительности и очень удобен для взаимодействия, что С++ может определять классы с точным расположением макетов данных членов, определенных стандартами сети или данных различных библиотек и протоколы. Эти данные часто поступают из-за пределов вашей программы на С++ и могут быть сгенерированы на другом языке. Такие протоколы связи и хранения не будут иметь "пробелов" для указателей на таблицы виртуальной диспетчеризации и, как обсуждалось ранее, даже если бы они это сделали, а компилятор каким-то образом позволял вам эффективно вводить правильные указатели для вашего процесса по входящим данным, что могло бы сорвать многопроцессорный доступ к данным. Коричневый, но практический код, основанный на указателе/​​размере, также будет более сложным и потенциально медленным.

Ответ 8

Кажется, что этот вопрос может иметь несколько ответов Виртуальные функции нельзя использовать чрезмерно - Почему?. По-моему, одна вещь, которая выделяется, заключается в том, что она просто увеличивает сложность с точки зрения знания того, что можно сделать с наследованием.

Ответ 9

Да, это из-за служебных накладных расходов. Виртуальные методы вызывают с использованием виртуальных таблиц и косвенных ссылок.

В Java все методы являются виртуальными и накладные расходы также присутствуют. Но, вопреки С++, компилятор JIT профилирует код во время выполнения и может встроить те методы, которые не используют это свойство. Таким образом, JVM знает, где это действительно необходимо, и где это не освобождает вас от принятия решения самостоятельно.

Ответ 10

Проблемы заключаются в том, что, хотя Java компилируется для кода, который выполняется на виртуальной машине, эта же гарантия не может быть выполнена для С++. Общепринято использовать С++ как более организованную замену для C, а C имеет перевод 1:1 на сборку.

Если вы считаете, что 9 из 10 микропроцессоров в мире не находятся в персональном компьютере или смартфоне, вы увидите эту проблему, если учесть, что существует много процессоров, которым необходим этот доступ на низком уровне.

С++ был разработан, чтобы избежать скрытой отсрочки, если вам это не понадобилось, тем самым сохраняя природу 1:1. Некоторые из первых С++-кода на самом деле имели промежуточный этап перехода на C перед запуском через компилятор C-to-assembly.

Ответ 11

Вызов методов Java намного эффективнее, чем С++ из-за оптимизации времени выполнения.

Нам нужно скомпилировать С++ в байт-код и запустить его на JVM.