Qt: Как использовать смарт-указатели Qt

У меня есть опыт "старомодного" программирования на C++ (т.е. я забочусь об указателях и управлении памятью). Тем не менее, я хочу использовать современные концепции.

Поскольку мое приложение интенсивно использует Qt, я хотел бы использовать смарт-указатели Qt. Однако меня несколько смущают умные указатели в целом и их использование в Qt.

1.) Насколько я понимаю, если я ухожу от QObject, мне лучше придерживаться дерева объектов и модели владения Qt и забыть о смарт-указателях. Верный?

2.) В C++ я могу обойтись std::shared_ptr и std::unique_ptr. Каковы эквивалентные интеллектуальные указатели в Qt?

Предположим, у меня есть следующий код:

QList<MyObject *> * foobar(MyOtherObject *ptr) {

   // do some stuff with MyOtherObject

   QList<MyObject* > ls = new QList<MyObject*>();
   for(int i=0;i<10;i++) {    
       MyObject* mi = new MyObject();
       ...
       ls.insert(mi);
   }
   return ls;
}

int main() {

   MyOtherObject* foo = new MyOtherObject();
   QList<MyObject*> *bar = foobar(foo);
  // do stuff
  // and don't care about cleaning up?!
}

3.) Как перевести приведенный выше фрагмент в версию с использованием смартпоинтеров?

4.) В частности: следует ли изменить подпись функции на использование смарт-указателей? Кажется, что создаются довольно сложные сигнатуры типов (возвращаемый тип и переданные аргументы). И что, если какая-то «устаревшая» функция вызывает другую функцию - лучше ли писать сигнатуры функций с необработанными указателями и использовать интеллектуальные указатели только «внутри» функций?

5.) Какой смартпоинтер должен заменить ls в функции foobar? Какой тип указателя следует использовать для mi, т.е. объекты, хранящиеся в QList?

Если вы используете Qt, вы более или менее вынуждены использовать родительскую систему Qt для управления памятью (= удалением) QObject. Я предлагаю вам следовать этому образцу. Для любых других объектов, размещенных в куче (которые не являются производными от QObject), используйте стандартные интеллектуальные указатели - std::shared_ptr и std::unique_ptr.

Jaa-c 05.04.2018 00:31

Почему вы вообще хотите использовать указатели в своем контейнере? Почему не только QList<MyObject>?

vahancho 05.04.2018 08:57

Почему бы не использовать QList<MyObject>? 1.) Просто для примера и 2.) это могут быть сложные объекты, созданные в куче по какой-либо причине, т.е. по размеру, или динамически созданные в другом месте.

ndbd 05.04.2018 09:40

Qt довольно старый; он представил множество классов до того, как появились стандартные эквиваленты std::. Это не значит, что они лучше. Тем не менее, QString, вероятно, немного проще в использовании.

MSalters 05.04.2018 10:09

Для примера: не возвращать указатель на QList <>, возвращать сам QList <>. 1) Компилятор должен иметь возможность выполнять RVO или std :: move 2) Довольно много классов (особенно контейнеров, включая QList) используют Неявный обмен, т.е. копирование объекта приведет к неглубокой копии (что очень дешево, как для памяти, так и для производительности). мудрый), который будет отсоединяться (т.е. глубокая копия), только если вы сделаете что-то для его изменения (выполните над ним неконстантные операции).

CharonX 09.04.2018 11:22

ndbd: вы должны принять ответ, он получил достаточно внимания и хорошие ответы.

user3606329 10.04.2018 12:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
6
1 635
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

Вы в значительной степени вынуждены использовать идиому Qt о владении необработанными указателями для объектов GUI, поскольку производные типы QWidget принимают на себя владение дочерними элементами.

В других местах вам следует по возможности избегать использования указателя типа любой. В большинстве случаев вы можете передать ссылку. Если вам нужно полиморфное владение, используйте std::unique_ptr. В cicrcumstances очень редкий у вас есть несколько независимых жизненных циклов, которым требуется владение общим ресурсом, для которого вы используете std::shared_ptr.

Классы коллекций Qt также плохо взаимодействуют с современными конструкциями C++, например.

extern QList<Foo> getFoos();

for (const Foo & foo : getFoos()) 
{ /*foo is a dangling reference here*/ }

for (const Foo & foo : std::as_const(getFoos()))
{ /*This is safe*/ }

Ваш фрагмент будет

std::vector<std::unique_ptr<MyObject>> foobar(MyOtherObject & obj) {

   // do some stuff with MyOtherObject

   std::vector<std::unique_ptr<MyObject>> ls;
   for(int i=0;i<10;i++)
   { 
       ls.emplace_back(std::make_unique<MyObject>());
       ...
   }
   return ls;
}

int main() {

   MyOtherObject foo = MyOtherObject;
   auto bar = foobar(foo);
  // do stuff
  // destructors do the cleanup automatically
}

Владение QObject - это все, от этого можно избавиться. Он есть, когда вам это нужно, но вам совсем не нужно использовать его, вытесняя его.

Kuba hasn't forgotten Monica 09.04.2018 21:18

1.) As far as I understand, if I derive from QObject, I should better stick to Qt's object tree and ownership model and forget about smartpointers. Correct?

Да. При использовании QObjects я бы рекомендовал полагаться на его родительско-дочернюю модель для управления памятью. Это работает хорошо, и вы не можете полностью избежать этого, так что используйте это.

2.) In C++ I can get by with std::shared_ptr and std::unique_ptr. What are the equivalent smart pointers in Qt?

Есть QSharedPointer и QScopedPointer, которые чем-то похожи на unique_ptr, но не поддерживают семантику перемещения. IMHO нет причин использовать их в настоящее время, просто используйте интеллектуальные указатели, предоставляемые стандартной библиотекой (для управления временем жизни объектов, которые не являются производными от QObject).

4.) In particular: Should I change function signature into using smartpointers? It seems to create quite complex type signatures (return type and passed arguments). Also what if some "legacy" function calls another function - is it better to write function signatures with raw pointers, and use smartpointers only "inside" functions?

Как правило - используйте интеллектуальные указатели только для управления памятью = используйте их только при наличии отношения собственности. Если вы просто передаете экземпляр функции, которая работает с ним, но не становится владельцем, передайте простой старый указатель.

Что касается вашего примера, сложно сказать без контекста. И ls, и mi могут быть unique_ptr. Лучше просто разместить ls в стеке. И даже лучше, избегайте использования QList и используйте std::vector или что-то в этом роде.

  1. As far as I understand, if I derive from QObject, I should better stick to Qt's object tree and ownership model and forget about smartpointers. Correct?

Лучше сказать «это зависит от обстоятельств». Во-первых, вы должны знать, что такое сходство потоков в Qt. Хороший пример - QThread worker.

QObject использует управление родительско-дочерней памятью, поэтому, если родительский элемент уничтожен, все его дочерние элементы также будут уничтожены. В «способе Qt» достаточно управлять только временем жизни корневого объекта. Это очень просто, когда вы создаете классы на основе QObject в куче (например, с оператором new). Но есть исключения:

  • Будьте осторожны с объектами которые созданы в стеке.
  • Классы с родительско-дочерними отношениями должны принадлежать одному потоку. Вы можете использовать метод QObject::moveToThread() для управления сходством потоков. Таким образом, если экземпляры связанных объектов должны принадлежать разным потокам, они могут не иметь родительско-дочерних отношений.

    Существует шаблон для автоматического удаления объектов, принадлежащих разным потокам. Например, если мы должны удалить p2 при уничтожении p1:

    QThread t;
    t.start();
    QObject *p1 = new MyClass1{};
    p1->moveToThread( &t );
    p1->doSomeWork();
    QObject *p2 = new MyClass2{};
    QObject::connect( p1, &QObject::destroyed, p2, &QObject::deleteLater ); // here
    

    При удалении p2 объект p1 будет уничтожен.

    Мы использовали полезный метод выше: QObject::deleteLater(). Он планирует удаление объекта как можно скорее. Его можно использовать в нескольких случаях:

  • Вам нужно удалить объект из слота или во время выдачи сигнала. Такие объекты не следует удалять напрямую, поскольку это может вызвать проблемы. Пример: кнопка самоуничтожения (где-то в MyClass):

    auto btn = new QPushButton{/*...*/};
    QObject::connect( btn, &QPushButton::clicked, this, &MyClass::onClicked );
    
    void MyClass::onClicked()
    {
        auto p = qobject_cast<QPushButton *>( sender() );
        // delete p; // Will cause error, because it is forbidden to delete objects during signal/slot calls
        p->deleteLater(); // OK, deletion will occurs after exit from the slot and will be correct
    }
    

Для работы с указателем на основе QObject есть помощник QPointer. Он не удалит объект автоматически, но будет действителен все время. Он будет содержать ссылку на объект или nullptr. Когда объект удаляется, все его экземпляры QPointer автоматически обнуляются. Пример:

class Foo : public QObject { /* ... */ };

class Bar
{
public:
    void setup( Foo * foo )
    {
        p = foo;
    }

    void use()
    {
        if ( p )
        {
            p->doSomething(); // Safe
        }
    }

private:
    QPointer<Foo> p;
};
// ...
Foo * foo = new Foo{};
Bar bar;
bar.setup( foo );
delete foo; // or not delete
bar.use();  // OK, it's safe

Обратите внимание, что QPointer использует сигнал QObject::destroyed() для управления внутренней ссылкой, поэтому его использование для хранения большого количества объектов может вызвать проблемы с производительностью (только при массовом создании / уничтожении). Производительность с доступом к этим указателям такая же, как с необработанными указателями.

  1. In C++ I can get by with std::shared_ptr and std::unique_ptr. What are the equivalent smart pointers in Qt?

Да, есть QSharedPointer и QScopedPointer, которые работают аналогичным образом. Некоторые преимущества использования этих указателей включают пользовательские удалители, например QScopedPointerDeleter, QScopedPointerArrayDeleter, QScopedPointerPodDeleter, QScopedPointerDeleteLater.

Также вы можете использовать что-то вроде этого для классов на основе QObject, если вам нужно отложенное удаление:

QSharedPointer<MyObject>(new MyObject, &QObject::deleteLater);

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

class Foo : public QObject
{
QScopedPointer<MyClass> p;

public:
Foo()
    : p{ new MyClass(this) } // NO!!! Use new MyClass() without specifying parent
// Object lifetime is tracked by QScopedPointer
{}
};

Если у вас есть список необработанных указателей, вы можете использовать помощники, такие как qDeleteAll(), для очистки. Пример:

QList<MyObject *> list;
//...
qDeleteAll( list );
list.clear();

3.

4.

5.

Это зависит от вашего дизайна и стиля кода C++. Персональный подход, из моего опыта:

  1. Используйте родительский-дочерний только для классов на основе QWidget.
  2. Используйте общие указатели только тогда, когда вы не можете контролировать время жизни объекта
  3. Во всех остальных случаях - используйте уникальные указатели.
  4. Чтобы поделиться чем-то между двумя классами - используйте Рекомендации.
  5. В случае сложной логики - умные указатели. Но перепроверьте для циклов (в большинстве случаев используйте слабые указатели)

Не стесняйтесь спрашивать в комментариях о любых разъяснениях.

Во-первых, современный C++ позволяет использовать значения, и Qt это поддерживает. Таким образом, по умолчанию следует использовать QObject, как если бы они были неподвижными, не копируемыми значениями. Фактически, ваш фрагмент вообще не требует явного управления памятью. И это совершенно не противоречит тому факту, что у объектов есть родитель.

#include <QObject>
#include <list>

using MyObject = QObject;
using MyOtherObject = QObject;

std::list<MyObject> makeObjects(MyOtherObject *other, QObject *parent = {}) {
  std::list<MyObject> list;
  for (int i = 0; i < 10; ++i) {
    #if __cplusplus >= 201703L // C++17 allows more concise code
    auto &obj = list.emplace_back(parent);
    #else
    auto &obj = (list.emplace_back(parent), list.back());
    #endif
    //...
  }
  return list;
}

int main() {
  MyOtherObject other;
  auto objects = makeObjects(&other, &other);
  //...
  objects.erase(objects.begin()); // the container manages lifetimes
  //
}

C++ имеет строгий порядок уничтожения объектов, и objects гарантированно будет уничтожен до other. Таким образом, к моменту запуска other.~QObject() дочерние элементы MyObject отсутствуют, и, следовательно, нет проблем с двойным удалением.

В общем, вот жизнеспособные подходы к хранению коллекций QObject с их требованиями:

  1. std::array - все элементы одного типа, фиксированного размера, не могут быть возвращены

  2. std::list - все элементы одного типа, не имеет RandomAccessIterator, объекты принадлежат контейнеру

  3. std::deque - все элементы одного типа, имеют RandomAccessIterator, но не допускают erase, так как ваши объекты не являются MoveAssignable (но, конечно, могут быть очищены / уничтожены), контейнер владеет объектами

  4. std::vector<std::unique_ptr<BaseClass>> - элементы любого типа, т.е. это полиморфный контейнер, контейнер владеет объектами

  5. std::vector<QObject*> или QObjectList - не владеющий, не отслеживающий контейнер. Код Qt заполнен QObjectList = QList<QObject*>.

  6. QObject (sic!) - элементы любого типа QObject, контейнер опционально владеет объектами, контейнер отслеживает время жизни объекта, указатель элемента может использоваться для удаления объекта из контейнера; только один контейнер может содержать данный объект, использует чистый вектор для хранения объектов, и, таким образом, добавление / удаление дочерних элементов - это O(N).

При хранении самих объектов, а не их коллекций, когда время жизни объекта такое же, как и у содержащей его области, проще всего сохранить их как значения. Например:

class ButtonGrid : public QWidget {
  static constexpr int const N = 3;
  QGridLayout m_gridLayout{this};
  QLabel m_label;
  std::array<QPushButton, N*N> m_buttons;
public:
  ButtonGrid(QWidget *parent = {}) : QWidget{parent} {
     int r = 0, c = 0;
     m_gridLayout.addWidget(&m_label, r, c, 1, N);
     r ++;
     for (auto &b : m_buttons) {
        m_gridLayout.addWidget(&b, r, c);
        C++;
        if (c == N)
           c = 0, r ++;
     }
  }
};

Теперь, чтобы ответить на ваши вопросы:

  1. Должен ли я лучше придерживаться дерева объектов и модели владения Qt? В этом вопросе нет выбора: эта модель есть, и ее нельзя отключить. Но он разработан как универсальный, и вы можете его упредить. Модель владения QObject гарантирует только то, что дети не переживут родителя. Это предотвращает утечку ресурсов. Вы можете покончить с жизнью детей до того, как умрет родитель. Вы также можете иметь объекты без родителей.

  2. Каковы эквивалентные интеллектуальные указатели в Qt? Неважно. Вы пишете C++ - используйте std::shared_ptr и std::unique_ptr. Нет никакой пользы от использования эквивалентов Qt, начиная с Qt 5.7: начиная с этой версии, Qt требует поддержки C++ 11 в компиляторе, и, следовательно, эти указатели должны поддерживаться.

  3. Как перевести приведенный выше фрагмент в версию с использованием умных указателей? В этом нет необходимости. Держите предметы по стоимости. Возможно, вам понадобится другой фрагмент, который действительно потребовал бы использования интеллектуальных указателей.

  4. Должен ли я изменить подпись функции на использование интеллектуальных указателей? Нет. Вы не мотивировали использование какого-либо умного указателя.

  5. N / A

Другие вопросы по теме