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

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

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

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

Мне интересен контекст С++ или Java.


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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

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();
    }
}

Теперь виджет не будет запущен в NullReferenceException позже.

Ответ 3

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

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

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

Ответ 4

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

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

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

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

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

Ответ 5

В С++: -

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

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

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

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

Ответ 6

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

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

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
};

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

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

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

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

Ответ 7

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

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

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

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

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

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

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

Ответ 8

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

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

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

Ответ 9

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

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