Несколько дней новогоднего волшебства:
Успейте начать обучение в 2018-ом году со скидкой до 30%!
Выбрать курс

Модификаторы virtual

VKС++Deep4_21.06_SITE_3.png

Делюсь с вами рассказом своего коллеги - разработчика С++. Уверен, информация пригодится вам в проектах!

Наконец-то пришёл долгожданный SDK от компании-партнёра. Проблем с интеграцией в наш проект не возникло. Началась работа над имплементацией на нашей стороне.

Собственно говоря, всё представлялось исключительно чётким и ясным: мы наследуем базовый класс из SDK нашего партнёра, имплементируем необходимые интерфейсные методы, добавляем свои... Работа заняла пару-тройку дней. Настало время тестирования.

Практически сразу же обнаружилась серьёзнейшая проблема: размер кучи (heap) безразмерно рос, приложение падало после нескольких минут работы.

Стали всё проверять и перепроверять

Наследуем базовый класс, имплементируем интерфейсные методы, несколько методов переопределяем, инстанциированный объект класса передаём обратно в SDK. Всё делается буквально по букварю, а приложение вылетает из-за переполнения памяти.

Обратились за помощью в компанию-партнёр. Ответ не заставил себя долго ждать: у нас всё протестировано и отлично работает. Оснований не доверять нет.

Мистика какая-то!

Расход памяти растёт при создании и удалении объектов класса, который наследует базовый класс из SDK. Тогда как по смыслу, объекты должны обрабатываться и удаляться из памяти.

Объекты создаются. Помещаются в очередь. Асинхронно обрабатываются. После чего удаляются из очереди, и память должна освобождаться. Записанные логи полностью всё подтверждают.

Почему же тогда расход памяти постоянно только растет? Где происходит утечка памяти?

Продолжаем расследовать

Добавляем деструктор в наш класс, наследующий базовый класс из SDK. При этом не забываем прописать модификатор virtual. Компилируем и собираем проект заново. Ставим брейкпоинт в наш деструктор, пусть он и пустой. Запускаем приложение на исполнение... И не видим ни одного останова в нашем деструкторе!

Как же так?

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

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

Однако, деструктор нашего класса не вызывается. Это больше, чем факт, потому что так происходит на самом деле... Предпринимаем несколько раундов переговоров с компанией-партнёром – ответ один и тот же: у нас всё протестировано и работает.

И тут вдруг буквально краем глаза, замечаем, что в заголовочном файле из SDK деструктор базового класса определён без модификатора virtual.

Семён Семёныч, ну как же так?!

Мы наследуем этот базовый класс, инстанциируем его, полученный объект передаём обратно в SDK, что вызывает потерю типа; объект приводится к базовому классу. Впоследствии, когда объект удаляется, вызывается только деструктор базового класса, освобождается не вся память!

Добавление модификатора virtual к деструктору нашего класса ровным счётом ничего не даёт, т.к. SDK скомпилирован таким образом, что деструктор базового класса не занесён в таблицу виртуальных функций класса!

Finita la commedia!

Объясняем проблему компании-партнёру, ждём новый релиз SDK… Вот такой коварный бывает C++! Не забывайте добавлять модификаторы virtual к методам базового класса и его деструктору! Это поможет избежать многих неочевидных проблем и часов отладки!

Есть вопрос? Напишите в комментариях!

Автор
2 комментария
0

"Не забывайте добавлять модификаторы virtual к методам базового класса и его деструктору!" - хочется предостеречь от добавления virtual к всему подряд. Если не планируется использовать парадигму вызова виртуальных методов, то не стоит добавлять virtual к деструктору и методам базового класса, так как это влечет накладные расходы на вызов методов через таблицу виртуальных функций. Однако, в википедии https://ru.wikipedia.org/wiki/%D0%A2%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D0%B0_%D0%B2%D0%B8%D1%80%D1%82%D1%83%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D1%85_%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D0%BE%D0%B2 указано, что "компиляторы обычно избегают использования vtable всегда, когда вызов может быть выполнен во время компиляции". Но правильно ли я понимаю, что сама таблица виртуальных функций будет создана автоматически при наличии хотя бы одного виртуального метода в классе (в том числе и деструктора)?

0

Михаил, Вы совершенно правы - не стоит перебарщивать и добавлять virtual везде и всюду. Конкретно по деструктору есть рекомендация в C++ guideline: C.35: A base class destructor should be either public and virtual, or protected and nonvirtual https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-dtor-virtual

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

Для комментирования необходимо авторизоваться