Почему в C++/Qt нельзя просто взять и забыть про сырые указатели

от автора

Современный 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.

Что пойдет не так?

  1. QScopedPointer честно удалит объект при выходе из функции.

  2. Но parent (объект Qt) все еще хранит указатель на этот label в своем внутреннем списке детей (children()).

  3. Когда придет время удалять parent (например, при закрытии окна), он попытается вызвать деструктор для уже удаленного label.

  4. Результат: 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 может решить, что теперь он владеет этим объектом:

  1. JavaScript-сборщик мусора (GC) видит, что объект больше не используется в QML-коде.

  2. GC удаляет объект.

  3. Ваш C++ указатель превращается в «тыкву» (dangling pointer).

  4. Результат: Непредсказуемый краш спустя случайное время после вызова GC.

Чтобы этого избежать, разработчики вынуждены либо всегда назначать родителя, либо явно вызывать QQmlEngine::setObjectOwnership(obj, QQmlEngine::CppOwnership). Умные указатели C++ здесь бессильны, так как они не могут контролировать сборщик мусора JavaScript.

7. Почему внутри Qt-проектов лучше выбирать Q-аналоги

Если вам все же нужно использовать умные указатели (например, для объектов без родителя), часто возникает дилемма: std::unique_ptr или QScopedPointer? В контексте Qt-разработки у вторых есть преимущества:

  1. Интеграция с контейнерами Qt: Классы вроде QList исторически лучше оптимизированы под работу с объектами, управляемыми через QSharedPointer.

  2. Бинарная совместимость (ABI): Q-аналоги гарантируют стабильность в рамках минорных версий Qt, что важно для разработки библиотек.

  3. Кастомизация удаления: QScopedPointer позволяет удобно использовать QScopedPointerDeleteLater, что критично для объектов вроде QNetworkReply, которые нельзя удалять немедленно.

8. Когда стоит отдать предпочтение умным указателям из std::?

Несмотря на мощь Qt, стандартная библиотека C++ (STL) в некоторых случаях оказывается эффективнее и правильнее:

  1. Производительность: std::unique_ptr имеет нулевой оверхед (zero-cost abstraction). Он чуть легче, чем QScopedPointer.

  2. Стандартные алгоритмы и библиотеки: Если вы используете сторонние библиотеки (Boost, OpenCV), они ожидают std-указатели.

  3. Бизнес-логика (Domain Model): Код, не завязанный на GUI и QObject, лучше писать на «чистом» C++. Это делает логику переносимой и упрощает юнит-тестирование.

  4. Современные возможности C++: std::shared_ptr поддерживает std::make_shared, что позволяет выделить память под объект и счетчик ссылок одним блоком.

Итоговые рекомендации

Чтобы не запутаться в двух системах управления памятью, следуйте этому простому алгоритму:

  1. Создаете QObject с родителем? Используйте сырой указатель. Памятью управляет родитель. new QLabel("Text", this) — это норма.

  2. Передаете объект в QML? Убедитесь, что у него есть родитель, или явно установите CppOwnership. В QML почти всегда «гуляют» сырые указатели.

  3. Создаете QObject БЕЗ родителя? Используйте QScopedPointer (или std::unique_ptr). Это защитит от утечек.

  4. Нужно следить за объектом, который может быть удален кем-то другим? Используйте QPointer. Это единственный безопасный способ проверить, жив ли еще виджет.

  5. Объект не наследует QObject (Бизнес-логика)? Используйте std::unique_ptr для владения и std::shared_ptr для разделяемых ресурсов.

  6. Нужно передать объект в другой поток через сигналы? Используйте QSharedPointer. Это «родной» способ Qt для многопоточности.

  7. Работаете с массивами данных в циклах? Используйте сырые указатели (uchar*, T*) для итерации.

Заключение

Программа на Qt — это баланс между классическим RAII и иерархическим владением. Попытка использовать только умные указатели — это борьба с фреймворком.

Понимая, где Qt берет ответственность на себя, а где оставляет её вам, вы сможете писать надежный код, не превращая его в нагромождение лишних абстракций.

ссылка на оригинал статьи https://habr.com/ru/articles/1026346/