Пишем свой пул объектов и наступаем на грабли
Что из себя представляет пул объектов? Обычно это некий список заранее созданных и переиспользуемых объектов. Нужен он для того, чтобы не тратить время на создание этих самых объектов во время выполнения программы. Т.е. схема работы такая: взяли объект из пула, поиспользовали, выкинули обратно в пул.
Наивная реализация
Пойдем простым путём. Возьмем std::stack
и создадим вокруг него нашу функциональную обертку:
// pool.hpp
#include <memory>
#include <stack>
/**
* @brief The Pool class simple pool of elements
*/
template <typename T>
class Pool {
public:
using item_ptr = std::shared_ptr<T>;
protected:
std::stack<item_ptr> pool; // active pool of items
protected:
item_ptr create() {
return std::make_shared<T>();
}
public:
item_ptr pop() {
if (this->pool.empty()) {
return this->create();
} else {
auto t = this->pool.top();
this->pool.pop();
return t;
}
}
void push(item_ptr && item) {
this->pool.push(item);
}
};
Отлично, стильно-модно-молодежно теперь у нас есть шаблонный пул для объектов типа T
. Заметьте, что в этой реализации пул не владеет объектами, которые он создает. Возможно это не совсем правильно, но этот вариант работы очень удобен, главное не забывать возвращать объекты в пул, иначе от него нет никакого прока.
Наступаем на грабли, раз
Начинаем использовать Pool
в проекте и замечаем, что иногда в релизной сборке мы крашимся. Благо у нас на проекте собираются дампы и можно понять, что там произошло.
Перейдем ближе к делу и рассмотрим пример использования, который приводит к проблеме:
class Widget: public QDialog {
...
};
Pool<Widget> pool;
void foo(int count) {
for (int i = 0; i < count; ++i) {
auto w = pool.pop();
w->setParent(qApp);
w->exec();
w->push(w);
}
}
У нас имеется глобальный доступ к пулу.
Когда мы закрываем приложение - получаем двойное освобождение памяти. Тот, кто имеет опыт с Qt
сразу заметит очень важную строку w->setParent(qApp);
. Причина, естественно, именно в ней.
Недолго думая добавляем специализацию пула для наследников QObject
:
template <typename T>
class QPool : public Pool<T> {
protected:
template <typename = typename std::enable_if<
std::is_base_of<QObject, T>::value>::type>
typename Pool<T>::item_ptr create() {
return std::shared_ptr<T>(new T(), [](T * t) { // set custom deleter
auto tmp = qobject_cast<QObject *>(t);
if (tmp) {
tmp->setParent(nullptr); // disable Qt parent system
tmp->deleteLater();
} else {
delete tmp;
}
});
}
public:
template <typename = typename std::enable_if<
std::is_base_of<QObject, T>::value>::type>
typename Pool<T>::item_ptr pop() {
if (this->pool.empty()) {
return this->create();
}
return Pool<T>::pop();
}
template <typename = typename std::enable_if<
std::is_base_of<QObject, T>::value>::type>
void push(typename Pool<T>::item_ptr && ptr) {
ptr->setParent(nullptr); // disable Qt parent system
Pool<T>::push(std::move(ptr));
}
};
Видно два основных дополнения:
- свой deleter;
- сброс родителя при возвращении объекта в пул (если не вернули - разбирайтесь сами);
- стало еще молодежней, даешь
SFINAE
в массы!
Что же тут вообще происходит? Методы QPool::pop
, QPool::push
, QPool::create
затеняют методы из Pool
, причем произойдет это только, если T
наследник QObject
. Если параметр шаблона не удовлетворяет этим условиям - получаем обычный Pool
А не лучше ли ошибку компиляции?
Выпускаем новую версию пула - не падает. Радуемся и пилим проект дальше.
Наступаем на грабли, два
В самый разгар релиза прилетает дамп. После некоторой медитации над ним, стало понятно, что с пулом что-то не так. Из сотен запусков только один привел к проблеме, а это уже серьезный повод задуматься.
Проблема проявлялась при работе с пулом объектов типа QWidget
, снова двойное освобождение памяти. Но мы же это уже исправили!
Между прочим, эта реализация без проблем отработала почти год - аргумент, как-никак. Но исправлять все равно надо. Еще пара часов медитаций и гуглений и вот он ответ: QWidget
значительно переопределяет метод QObject::setParent
, а метод-то этот не виртуальный! Выходит что пул вызывает совсем не то, что нужно для QWidget
, а это уже похоже на UB…
Добавим еще одну специализацию и вот оно, счастье!
Выводы, размышления и почему так получилось
Тут надо задать вопрос: “Эй, тимлид, а где тесты?” А их не было и сейчас нет на этот функционал…
Юнит-тестирование, возможно, выявило бы эти проблемы на более ранних этапах. А может и нет, кто бы догадался еще разные типы в тестах проверять?
В общем, история довольно поучительная. Проверять код нужно, любой код. Только тогда можно говорить о том, что он работает корректно. Но как точно удостовериться в корректности всех тестов? На этот вопрос тоже уже давно придумали ответ - формальные методы.