Заметки по языку с и с++

Умные указатели в Qt-коде: когда QSharedPointer, а когда std::shared_ptr

2 просмотров

Введение

В проектах на Qt разработчик неизбежно сталкивается с выбором: использовать собственные умные указатели Qt (QSharedPointer, QScopedPointer, QPointer) или указатели стандартной библиотеки (std::shared_ptr, std::unique_ptr, std::weak_ptr). Исторически Qt появился раньше, чем C++11 принёс стандартные умные указатели, поэтому в библиотеке есть собственные аналоги. Сегодня их совместное использование требует понимания тонкостей.

Суть концепции

Qt предоставляет несколько разных инструментов управления временем жизни объектов, и важно не путать их назначение:

  • QObject с родителем (parent) — собственная модель владения Qt, где родительский QObject автоматически удаляет всех своих детей в деструкторе. Это не умный указатель, а древовидная модель владения, встроенная прямо в QObject.
  • QPointer<T> — «слабая» ссылка на QObject-потомка, которая автоматически становится nullptr, если объект был удалён. Используется только для классов, унаследованных от QObject.
  • QSharedPointer<T> / QWeakPointer<T> — аналоги std::shared_ptr/std::weak_ptr, подсчёт ссылок, работают с любыми типами, не обязательно QObject.
  • std::shared_ptr<T> / std::unique_ptr<T> / std::weak_ptr<T> — стандартные умные указатели, переносимы между проектами, не зависят от Qt, лучше интегрируются со сторонними C++ библиотеками.
#include <QObject>
#include <QPointer>
#include <QSharedPointer>
#include <memory>
#include <iostream>

// Пример доменного класса, не связанного с QObject —
// для него естественны стандартные умные указатели.
class NetworkSession
{
public:
    explicit NetworkSession(const std::string& host) : host_(host)
    {
        std::cout << "Открыта сессия с " << host_ << std::endl;
    }
    ~NetworkSession()
    {
        std::cout << "Сессия с " << host_ << " закрыта" << std::endl;
    }
    void send(const std::string& data)
    {
        std::cout << "[" << host_ << "] -> " << data << std::endl;
    }
private:
    std::string host_;
};

// Виджет — наследник QObject, у него своя модель владения через parent
class Worker : public QObject
{
    Q_OBJECT
public:
    explicit Worker(QObject* parent = nullptr) : QObject(parent) {}
    void doWork() { std::cout << "Worker работает" << std::endl; }
};

void demoStdSharedPtr()
{
    // Доменная логика без связи с Qt-объектной моделью —
    // используем std::shared_ptr, он стандартен и переносим
    std::shared_ptr<NetworkSession> session =
        std::make_shared<NetworkSession>("example.com");

    auto sessionCopy = session; // подсчёт ссылок: refcount == 2
    sessionCopy->send("GET /api/data");
}

void demoQPointer()
{
    // Worker — QObject, владение задаётся родителем
    QObject parentObject;
    Worker* worker = new Worker(&parentObject);

    // QPointer следит за worker и сам обнулится, если worker удалится
    QPointer<Worker> watcher(worker);

    if (watcher) {
        watcher->doWork();
    }

    // parentObject удаляется здесь, а вместе с ней — и worker
}

int main()
{
    demoStdSharedPtr();
    demoQPointer();
    return 0;
}

Разбор примера

В demoStdSharedPtr() класс NetworkSession — обычный C++ класс, не имеющий отношения к объектной модели Qt. Для таких классов нет смысла использовать QSharedPointer: std::shared_ptr стандартен, лучше поддерживается инструментами анализа кода, и его проще передавать в код, не зависящий от Qt (например, в общую библиотеку алгоритмов).

В demoQPointer() класс Worker наследуется от QObject и получает владельца через конструктор (parent). Здесь работает родная модель Qt: когда parentObject выходит из области видимости и удаляется, Qt автоматически удаляет всех её детей, включая worker. QPointer<Worker> — это «слабое наблюдение»: после удаления worker указатель watcher сам станет nullptr, и проверка if (watcher) защитит от обращения к мёртвому объекту (избегая classic dangling pointer).

Когда что использовать — практическое правило

  • Класс наследуется от QObject и встроен в дерево виджетов/объектов → используйте родительскую модель владения Qt и QPointer для слабых наблюдений. Не оборачивайте QObject в QSharedPointer/std::shared_ptr, если у него уже есть parent — это создаст конфликт двух механизмов владения и может привести к двойному удалению.
  • Класс не наследуется от QObject, проект тесно завязан на Qt API (например, передаётся в сигналы/слоты, в QVariant, в Qt-контейнеры старого стиля) → можно использовать QSharedPointer, он интегрируется с QMetaType и сериализацией Qt.
  • Класс не наследуется от QObject, и код должен быть переносим / тестируем независимо от Qt (бизнес-логика, модели данных, сетевые сессии, парсеры) → используйте std::shared_ptr/std::unique_ptr. Это упрощает unit-тестирование без зависимости от QtTest и облегчает миграцию кода между проектами.
  • Единоличное владение без подсчёта ссылок → для QObject-объектов без родителя подойдёт QScopedPointer или просто std::unique_ptr (начиная с Qt 5 они взаимозаменяемы по смыслу, std::unique_ptr предпочтительнее в новом коде).

Подводные камни

  • Не смешивайте QSharedPointer и std::shared_ptr для одного и того же объекта — у них разные счётчики ссылок, и объект может быть удалён одним механизмом, пока другой ещё считает его живым.
  • QObject с родителем нельзя одновременно держать в shared_ptr с обычным удалением — родитель удалит объект сам, а shared_ptr попытается удалить его повторно. Если такая необходимость всё же есть, используйте shared_ptr с кастомным delete-функтором, который ничего не делает, либо вообще не назначайте parent.
  • QPointer работает только с QObject-потомками — для обычных C++ классов используйте std::weak_ptr.
  • В новом коде на Qt 6 многие разработчики сознательно выбирают std::shared_ptr/std::unique_ptr по умолчанию для не-QObject-классов, оставляя Qt-указатели только там, где требуется глубокая интеграция с Qt API (сигналы/слоты, QVariant, межпотоковые очереди событий).

================================================================================