Почему виртуальные функции не должны использоваться чрезмерно?

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

Какие ошибки и недостатки могут появиться из-за виртуальных функций?

Я интересуюсь контекстом C++ или Java.


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

12
задан curiousguy 14 August 2015 в 04:48
поделиться

8 ответов

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

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

В C++ вы должны объявить функцию виртуальной, но вполне допустимо использовать их, когда это уместно.

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

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

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

Плохо спроектированный код трудно поддерживать. Точка.

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

Итак, есть несколько правил, все с исключениями:

  • Держите ваши иерархии неглубокими. Высокие деревья приводят к запутанным классам.
  • В c++, если ваш класс имеет виртуальные функции, используйте виртуальный деструктор (если нет, это, вероятно, ошибка)
  • Как и в любой иерархии, придерживайтесь отношений "is-a" между производными и базовыми классами.
  • Вы должны знать, что виртуальная функция может вообще не быть вызвана... поэтому не добавляйте неявных ожиданий.
  • Трудно оспорить тот факт, что виртуальные функции работают медленнее. Они динамически связаны, поэтому часто так и происходит. Имеет ли это значение в большинстве случаев, на которые ссылаются, конечно, спорно. Вместо этого профилируйте и оптимизируйте :)
  • В C++ не используйте virtual, когда это не нужно. В обозначении функции виртуальной есть семантический смысл - не злоупотребляйте им. Дайте читателю понять, что "да, это может быть переопределено!".
  • Предпочитайте чистые виртуальные интерфейсы иерархии, в которой смешиваются реализации. Это чище и намного проще для понимания.

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

14
ответ дан 2 December 2019 в 04:16
поделиться

В C ++: -

  1. Виртуальные функции имеют небольшое снижение производительности. Обычно он слишком мал, чтобы иметь какое-либо значение, но в узком цикле он может быть значительным.

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

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

Теперь позвольте мне прояснить: я не говорю «не используйте виртуальные функции». Они являются жизненно важной и важной частью C ++. Просто помните о возможной сложности.

2
ответ дан 2 December 2019 в 04:16
поделиться

Недавно у нас был прекрасный пример того, как неправильное использование виртуальных функций приводит к ошибкам.

Существует общая библиотека с обработчиком сообщений:

class CMessageHandler {
public:
   virtual void OnException( std::exception& e );
   ///other irrelevant stuff
};

цель состоит в том, чтобы вы могли наследовать от этого класса и использовать его для специальной обработки ошибок:

class YourMessageHandler : public CMessageHandler {
public:
   virtual void OnException( std::exception& e ) { //custom reaction here }
};

Механизм обработки ошибок использует CMessageHandler * , поэтому он не заботится о фактическом типе объекта. Функция виртуальная, поэтому всякий раз, когда существует перегруженная версия, вызывается последняя.

Круто, правда? Да, это было до тех пор, пока разработчики разделяемой библиотеки не изменили базовый класс:

class CMessageHandler {
public:
   virtual void OnException( const std::exception& e ); //<-- notice const here
   ///other irrelevant stuff
};

... и перегрузки просто перестали работать.

Вы видите, что произошло? После изменения базового класса перегрузки перестали быть перегрузками с точки зрения C ++ - они стали новыми, другими, не связанными функциями .

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

Единственный способ устранить это раз и навсегда - это провести поиск по всей кодовой базе и отредактировать все соответствующие фрагменты кода.

2
ответ дан 2 December 2019 в 04:16
поделиться

Не знаю, где вы это читали, но имхо, дело вовсе не в производительности.

Может быть, это больше о «предпочтительной композиции для наследования» и проблемах, которые могут возникнуть, если ваши классы / методы не являются окончательными (я говорю здесь в основном о java), но на самом деле не предназначены для повторного использования.Есть много вещей, которые могут пойти не так:

  • Возможно, вы используете виртуальные методы в своей конструктор - как только они переопределены, ваш базовый класс вызывает переопределенный метод, который может использовать ресурсы инициализируется в подклассе конструктор - который запускается позже (возрастает NPE).

  • Представьте себе методы add и addAll в классе списка. addВсе звонки добавить много раз, и оба они виртуальны. Кто-то может переопределить их для подсчета сколько элементов было добавлено в все. Если вы не документируете это addAll вызывает добавление, разработчик может (и будет) переопределить как add, так и addAll (и добавьте немного counter ++ в их). Но теперь, если вы добавите все, каждый элемент считается дважды (добавить и addAll), что приводит к неправильному результаты и трудно найти ошибки.

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

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

Дополнительная информация о подобных вещах в Blochs Effective Java (этот конкретный пост был написан через несколько дней после того, как я прочитал пункт 16 («предпочитать композицию наследованию») и 17 («дизайн и документ для наследования или запретить это») - удивительная книга.

1
ответ дан 2 December 2019 в 04:16
поделиться

Время от времени я работал консультантом по той же системе C ++ в течение примерно 7 лет, проверяя работу примерно 4-5 программистов. Каждый раз, когда я возвращался, система становилась все хуже и хуже.В какой-то момент кто-то решил удалить все виртуальные функции и заменить их очень тупой фабричной / основанной на RTTI системой, которая, по сути, делала все, что уже делали виртуальные функции, но хуже, дороже, тысячи строк кода, много работы, много испытаний, ... Совершенно и совершенно бессмысленно, и явно вызвано страхом перед неизвестным.

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

Мораль: не борись с языком. Это дает вам вещи: используйте их.

0
ответ дан 2 December 2019 в 04:16
поделиться

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

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

(Еще одна проблема - проблема с производительностью, о которой вы упомянули, из-за v-table).

6
ответ дан 2 December 2019 в 04:16
поделиться

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

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

Хорошая идея - писать классы так, чтобы они были открытыми для расширения, но есть такая вещь, как too open . Тщательно планируя, какие функции являются виртуальными, вы можете контролировать (и защищать), как класс может быть расширен.

Ошибки и проблемы с обслуживанием возникают, когда класс расширяется таким образом, что нарушается контракт базового класса. Вот пример:

class Widget
{
    private WidgetThing _thing;

    public virtual void Initialize()
    {
        _thing = new WidgetThing();
    }
}

class DoubleWidget : Widget
{
    private WidgetThing _double;

    public override void Initialize()
    {
        // Whoops! Forgot to call base.Initalize()
        _double = new WidgetThing();
    }
}

Здесь DoubleWidget нарушил родительский класс, потому что Widget._thing имеет значение null. Есть довольно стандартный способ исправить это:

class Widget
{
    private WidgetThing _thing;

    public void Initialize()
    {
        _thing = new WidgetThing();
        OnInitialize();
    }

    protected virtual void OnInitialize() { }
}

class DoubleWidget : Widget
{
    private WidgetThing _double;

    protected override void OnInitialize()
    {
        _double = new WidgetThing();
    }
}

Теперь Widget не столкнется с NullReferenceException позже.

7
ответ дан 2 December 2019 в 04:16
поделиться

Я подозреваю, что вы неправильно поняли утверждение.

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

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

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

В Java по умолчанию используется «виртуальное» поведение (динамическая диспетчеризация). Однако JVM может оптимизировать вещи на лету и теоретически может устранить некоторые виртуальные вызовы, когда целевая идентификация ясна. Кроме того, final методы или методы в final классах часто могут быть разрешены для одной цели также во время компиляции.

3
ответ дан 2 December 2019 в 04:16
поделиться
Другие вопросы по тегам:

Похожие вопросы: