Модификаторы virtual
Делюсь с вами рассказом своего коллеги - разработчика С++. Уверен, информация пригодится вам в проектах!
Наконец-то пришёл долгожданный SDK от компании-партнёра. Проблем с интеграцией в наш проект не возникло. Началась работа над имплементацией на нашей стороне.
Собственно говоря, всё представлялось исключительно чётким и ясным: мы наследуем базовый класс из SDK нашего партнёра, имплементируем необходимые интерфейсные методы, добавляем свои... Работа заняла пару-тройку дней. Настало время тестирования.
Практически сразу же обнаружилась серьёзнейшая проблема: размер кучи (heap) безразмерно рос, приложение падало после нескольких минут работы.
Стали всё проверять и перепроверять
Наследуем базовый класс, имплементируем интерфейсные методы, несколько методов переопределяем, инстанциированный объект класса передаём обратно в SDK. Всё делается буквально по букварю, а приложение вылетает из-за переполнения памяти.
Обратились за помощью в компанию-партнёр. Ответ не заставил себя долго ждать: у нас всё протестировано и отлично работает. Оснований не доверять нет.
Мистика какая-то!
Расход памяти растёт при создании и удалении объектов класса, который наследует базовый класс из SDK. Тогда как по смыслу, объекты должны обрабатываться и удаляться из памяти.
Объекты создаются. Помещаются в очередь. Асинхронно обрабатываются. После чего удаляются из очереди, и память должна освобождаться. Записанные логи полностью всё подтверждают.
Почему же тогда расход памяти постоянно только растет? Где происходит утечка памяти?
Продолжаем расследовать
Добавляем деструктор в наш класс, наследующий базовый класс из SDK. При этом не забываем прописать модификатор virtual. Компилируем и собираем проект заново. Ставим брейкпоинт в наш деструктор, пусть он и пустой. Запускаем приложение на исполнение... И не видим ни одного останова в нашем деструкторе!
Как же так?
Ведь в любой литературе по C++ чётко прописывается, что при удалении объекта класса из памяти, будет первым вызван его собственный деструктор, а потом деструктор базового класса...
Что ж, это объясняет неограниченный рост расхода памяти. Но чёрт возьми, в логах явно видно, что объекты удаляются из памяти, размер очереди из объектов нашего класса не растёт неограниченно!
Однако, деструктор нашего класса не вызывается. Это больше, чем факт, потому что так происходит на самом деле... Предпринимаем несколько раундов переговоров с компанией-партнёром – ответ один и тот же: у нас всё протестировано и работает.
И тут вдруг буквально краем глаза, замечаем, что в заголовочном файле из SDK деструктор базового класса определён без модификатора virtual.
Семён Семёныч, ну как же так?!
Мы наследуем этот базовый класс, инстанциируем его, полученный объект передаём обратно в SDK, что вызывает потерю типа; объект приводится к базовому классу. Впоследствии, когда объект удаляется, вызывается только деструктор базового класса, освобождается не вся память!
Добавление модификатора virtual к деструктору нашего класса ровным счётом ничего не даёт, т.к. SDK скомпилирован таким образом, что деструктор базового класса не занесён в таблицу виртуальных функций класса!
Finita la commedia!
Объясняем проблему компании-партнёру, ждём новый релиз SDK… Вот такой коварный бывает C++! Не забывайте добавлять модификаторы virtual к методам базового класса и его деструктору! Это поможет избежать многих неочевидных проблем и часов отладки!
Есть вопрос? Напишите в комментариях!