Современный C++ (11/14/17/20…) настойчиво учит нас: «Забудьте про new и delete, используйте умные указатели». Это отличный совет для чистого C++, но как только вы открываете документацию Qt, на вас снова прыгают T*.
Почему даже в 2026 году невозможно написать серьезное приложение на Qt, используя исключительно умные указатели? Давайте разберемся, где «умный» код ломает логику фреймворка.
1. Конфликт систем владения: QObject Parent-Child vs Smart Pointers
Главная фишка Qt — иерархия объектов. Когда вы передаете родителя в конструктор QObject (или наследников вроде QWidget), вы делегируете управление памятью этому родителю.
Проблема «Двойного удаления»
Представим, что мы решили быть «правильными» и использовали QScopedPointer (аналог std::unique_ptr) там, где уже есть родитель:
void createUI(QWidget* parent) { // Владение передано parent, но QScopedPointer тоже считает себя владельцем QScopedPointer<QLabel> label(new QLabel("Hello", parent)); // ... работа с label ...} // Тут QScopedPointer выходит из области видимости и вызывает delete.
Что пойдет не так?
-
QScopedPointerчестно удалит объект при выходе из функции. -
Но
parent(объект Qt) все еще хранит указатель на этотlabelв своем внутреннем списке детей (children()). -
Когда придет время удалять
parent(например, при закрытии окна), он попытается вызвать деструктор для уже удаленногоlabel. -
Результат: Crash / Double Free.
В Qt владение объектом — это динамическая древовидная структура, а умные указатели реализуют статическое или счетное владение. Они просто не знают о существовании QObject::parent().
2. Динамическое дерево объектов и QPointer
В GUI-приложениях время жизни объектов часто непредсказуемо. Кнопка может быть удалена из-за смены вкладки, закрытия диалога или по сигналу от сервера.
Если вам нужно хранить ссылку на объект, которым вы не владеете, QSharedPointer вам не поможет (он продлит жизнь объекту, который должен был умереть), а QWeakPointer работает эффективно только в паре с QSharedPointer.
Здесь на сцену выходит QPointer<T>. Это уникальный для Qt «слабый» указатель:
-
Он не владеет объектом.
-
Он автоматически обнуляется (
null), когда целевойQObjectудаляется (черезdeleteили родителем). -
Это возможно благодаря подписке на сигнал
destroyed().
Без понимания работы сырых указателей «под капотом» и жизненного цикла QObject правильно использовать этот инструмент невозможно.
3. Макросы и метаобъектная система (MOC)
Qt — это не просто библиотека, это расширение языка. Макросы Q_OBJECT, slots, signals завязаны на метаинформацию и работу с сырыми адресами в памяти.
Pimpl и d_ptr
Внутри Qt повсеместно используется паттерн Pimpl (Private Implementation). В исходниках любого класса Qt вы увидите d_ptr. Исторически это сырой указатель, обернутый в макросы Q_D и Q_Q. Хотя в новых версиях Qt иногда применяется QScopedPointer, использование std-аналогов там затруднено требованиями к бинарной совместимости (ABI).
4. Сигналы, слоты и sender()
Метод sender(), возвращающий QObject*, — еще одна причина существования сырых указателей. Попытка обернуть этот результат в умный контейнер — это попытка забрать владение у системы, которая им уже управляет. Это кратчайший путь к падению приложения.
5. Взаимодействие с низкоуровневым API
При работе с QImage::bits() или OpenGL-контекстом вы получаете сырой доступ к памяти.
uchar* scanline = image.scanLine(y);for(int x = 0; x < width; ++x) { // Прямая работа с памятью — единственный путь к 60 FPS scanline[x] = process(scanline[x]); }
Любой умный указатель в таком цикле создаст оверхед, который уничтожит преимущества C++.
6. QML и неявная передача владения (Ownership)
Если вы используете QML, ситуация с указателями становится еще острее. Когда вы передаете QObject* из C++ в QML (через сигналы, методы или свойства), вступает в силу механизм QQmlEngine::ObjectOwnership.
Ловушка JavaScript-владения
Если объект был создан в C++, но у него нет родителя (parent == nullptr), то при передаче в QML движок JavaScript может решить, что теперь он владеет этим объектом:
-
JavaScript-сборщик мусора (GC) видит, что объект больше не используется в QML-коде.
-
GC удаляет объект.
-
Ваш C++ указатель превращается в «тыкву» (dangling pointer).
-
Результат: Непредсказуемый краш спустя случайное время после вызова GC.
Чтобы этого избежать, разработчики вынуждены либо всегда назначать родителя, либо явно вызывать QQmlEngine::setObjectOwnership(obj, QQmlEngine::CppOwnership). Умные указатели C++ здесь бессильны, так как они не могут контролировать сборщик мусора JavaScript.
7. Почему внутри Qt-проектов лучше выбирать Q-аналоги
Если вам все же нужно использовать умные указатели (например, для объектов без родителя), часто возникает дилемма: std::unique_ptr или QScopedPointer? В контексте Qt-разработки у вторых есть преимущества:
-
Интеграция с контейнерами Qt: Классы вроде
QListисторически лучше оптимизированы под работу с объектами, управляемыми черезQSharedPointer. -
Бинарная совместимость (ABI):
Q-аналоги гарантируют стабильность в рамках минорных версий Qt, что важно для разработки библиотек. -
Кастомизация удаления:
QScopedPointerпозволяет удобно использоватьQScopedPointerDeleteLater, что критично для объектов вродеQNetworkReply, которые нельзя удалять немедленно.
8. Когда стоит отдать предпочтение умным указателям из std::?
Несмотря на мощь Qt, стандартная библиотека C++ (STL) в некоторых случаях оказывается эффективнее и правильнее:
-
Производительность:
std::unique_ptrимеет нулевой оверхед (zero-cost abstraction). Он чуть легче, чемQScopedPointer. -
Стандартные алгоритмы и библиотеки: Если вы используете сторонние библиотеки (Boost, OpenCV), они ожидают
std-указатели. -
Бизнес-логика (Domain Model): Код, не завязанный на GUI и
QObject, лучше писать на «чистом» C++. Это делает логику переносимой и упрощает юнит-тестирование. -
Современные возможности C++:
std::shared_ptrподдерживаетstd::make_shared, что позволяет выделить память под объект и счетчик ссылок одним блоком.
Итоговые рекомендации
Чтобы не запутаться в двух системах управления памятью, следуйте этому простому алгоритму:
-
Создаете
QObjectс родителем? Используйте сырой указатель. Памятью управляет родитель.new QLabel("Text", this)— это норма. -
Передаете объект в QML? Убедитесь, что у него есть родитель, или явно установите CppOwnership. В QML почти всегда «гуляют» сырые указатели.
-
Создаете
QObjectБЕЗ родителя? ИспользуйтеQScopedPointer(илиstd::unique_ptr). Это защитит от утечек. -
Нужно следить за объектом, который может быть удален кем-то другим? Используйте
QPointer. Это единственный безопасный способ проверить, жив ли еще виджет. -
Объект не наследует
QObject(Бизнес-логика)? Используйтеstd::unique_ptrдля владения иstd::shared_ptrдля разделяемых ресурсов. -
Нужно передать объект в другой поток через сигналы? Используйте
QSharedPointer. Это «родной» способ Qt для многопоточности. -
Работаете с массивами данных в циклах? Используйте сырые указатели (
uchar*,T*) для итерации.
Заключение
Программа на Qt — это баланс между классическим RAII и иерархическим владением. Попытка использовать только умные указатели — это борьба с фреймворком.
Понимая, где Qt берет ответственность на себя, а где оставляет её вам, вы сможете писать надежный код, не превращая его в нагромождение лишних абстракций.
ссылка на оригинал статьи https://habr.com/ru/articles/1026346/