Есть ли причина не делать функцию-член виртуальной?

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

С другой стороны, меня пару раз укусили за то, что я забыл сделать виртуальную функцию, которая должна быть виртуальной. И это кажется большим аргументом, чем производительность. Итак, есть ли причина не делать функции-члены виртуальными по умолчанию?


person Jason Baker    schedule 15.11.2008    source источник
comment
Виртуальные функции снижают производительность двумя способами: во-первых, косвенным вызовом функции, что, как вы говорите, не так уж и плохо. Во-вторых, компилятор не может встроить вызов функции. У меня есть случай, когда GCC 4.3 создает код в 10 раз быстрее без виртуального. Если он может быть встроен, он может выполнять множество других опций.   -  person Zan Lynx    schedule 15.11.2008
comment
[Язык программирования D][digitalmars.com/d/] (утверждается, что это обновление C++) сделать каждую функцию виртуальной по умолчанию и, если это не нужно, компилятор оптимизирует ее. Следовательно, теоретически это может быть сделано для C++ в будущем.   -  person Alexander Malakhov    schedule 26.03.2010


Ответы (7)


Один из способов прочитать ваши вопросы: «Почему С++ не делает каждую функцию виртуальной по умолчанию, если только программист не переопределяет это значение по умолчанию». Без консультации с моей копией «Дизайн и эволюция C++»: это добавит дополнительное хранилище для каждого класса, если только каждая функция-член не будет сделана невиртуальной. Мне кажется, что это потребовало бы больше усилий при реализации компилятора и замедлило бы внедрение C++, предоставив корм для одержимых производительностью (я отношу себя к этой группе).

Другой способ прочитать ваши вопросы: «Почему программисты на С++ не делают каждую функцию виртуальной, если у них нет очень веских причин не делать этого?» Оправдание производительности, вероятно, является причиной. В зависимости от вашего приложения и домена это может быть хорошей причиной или нет. Например, часть моей команды работает на биржах рыночных данных. При скорости более 100 000 сообщений в секунду в одном потоке накладные расходы виртуальных функций были бы неприемлемыми. Другие части моей команды работают в сложной торговой инфраструктуре. Сделать большинство функций виртуальными, вероятно, является хорошей идеей в этом контексте, поскольку дополнительная гибкость превосходит микрооптимизацию.

person coryan    schedule 15.11.2008

Страуструп, разработчик языка, говорит:

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

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

См. «Дизайн и эволюция C++» для более подробного обоснования дизайна.

person David Norman    schedule 15.11.2008

Есть несколько причин.

Во-первых, производительность: да, накладные расходы виртуальной функции относительно невелики в отдельности. Но это также предотвращает встраивание компилятора, и это является огромным источником оптимизации в C++. Стандартная библиотека C++ работает так же хорошо, как она, потому что она может встроить десятки и десятки небольших однострочников, из которых она состоит. Кроме того, класс с виртуальными методами не является типом данных POD, поэтому к нему применяется множество ограничений. Его нельзя скопировать просто с помощью memcpy, его создание становится дороже и занимает больше места. Есть много вещей, которые внезапно становятся незаконными или менее эффективными, когда задействован тип, отличный от POD.

И во-вторых, хорошая практика ООП. Суть класса в том, что он делает некую абстракцию, скрывает свои внутренние детали и дает гарантию, что «этот класс будет вести себя так-то и так-то и всегда будет поддерживать эти инварианты. Он никогда оказаться в недопустимом состоянии». Это довольно сложно реализовать, если вы позволяете другим переопределять любую функцию-член. Функции-члены, которые вы определили в классе, обеспечивают сохранение инварианта. Если бы нас это не заботило, мы могли бы просто сделать внутренние данные-члены общедоступными и позволить людям манипулировать ими по своему усмотрению. Но мы хотим, чтобы наш класс был последовательным. А это значит, что мы должны указать поведение его публичного интерфейса. Это может включать в себя конкретные точки настройки, делая отдельные функции виртуальными, но почти всегда это также включает в себя невиртуальные методы, чтобы они могли выполнять работу по обеспечению сохранения инварианта. Хорошим примером этого является идиома невиртуального интерфейса: http://www.gotw.ca/publications/mill18.htm

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

person jalf    schedule 15.11.2008

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

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

Примеры вещей, которые вы можете переносимо делать с экземпляром класса POD:

  • скопируйте его с помощью memcpy (при условии, что целевой адрес имеет достаточное выравнивание).
  • поля доступа с помощью offsetof()
  • в общем, рассматривайте это как последовательность char
  • ... um
  • вот об этом. Я уверен, что я что-то забыл.

Другие вещи, которые люди упомянули, с которыми я согласен:

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

  • Многие методы не предназначены для переопределения: то же самое.

Кроме того, даже когда вещи предназначены для подкласса/переопределения, они не обязательно предназначены для полиморфизма во время выполнения. Очень редко, несмотря на то, что говорит лучшая практика ООП, вам нужно наследование для повторного использования кода. Например, если вы используете CRTP для имитации динамической привязки. Итак, опять же, вы не хотите, чтобы ваш класс хорошо работал с полиморфизмом времени выполнения, делая его методы виртуальными, когда их никогда не следует вызывать таким образом.

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

Это сложная проблема при разработке общедоступного API, потому что переключение метода с одного метода на другой является критическим изменением, поэтому вы должны сделать это правильно с первого раза. Но вы не обязательно знаете, прежде чем у вас появятся пользователи, захотят ли ваши пользователи «полиморфировать» ваши классы. Хо хм. Контейнерный подход STL, заключающийся в определении абстрактных интерфейсов и полном запрете наследования, безопасен, но иногда требует от пользователей большего набора текста.

person Steve Jessop    schedule 15.11.2008

Следующий пост в основном является мнением, но здесь идет:

Объектно-ориентированный дизайн — это три вещи, и инкапсуляция (сокрытие информации) — первая из них. Если дизайн класса не является твердым в этом, то остальное не имеет большого значения.

Ранее уже говорилось, что «наследование нарушает инкапсуляцию» (Alan Snyder '86). Хорошее обсуждение этого вопроса содержится в книге о группе из четырех шаблонов проектирования. Класс должен быть разработан для поддержки наследования очень специфическим образом. В противном случае вы открываете возможность неправомерного использования наследниками.

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

person Matt Brunell    schedule 15.11.2008

При разработке иерархии классов может иметь смысл написать функцию, которую не следует переопределять. Например, если вы используете шаблон «метод шаблона», где у вас есть общедоступный метод, который вызывает несколько частных виртуальных методов. Вы бы не хотели, чтобы производные классы переопределяли это; все должны использовать базовое определение.

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

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

Делая метод невиртуальным, вы сообщаете, как его следует использовать.

person JohnMcG    schedule 15.11.2008

Идиома невиртуального интерфейса использует невиртуальные методы. Для получения дополнительной информации обратитесь к статье Херба Саттера «Виртуальность».

http://www.gotw.ca/publications/mill18.htm

И комментарии к идиоме NVI:

http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.3 http://accu.org/index.php/journals/269 [см. подраздел]

person Community    schedule 15.11.2008
comment
NVI просто заявляет, что виртуальные функции должны быть защищены. - person Nicola Bonelli; 15.11.2008