Qt:如何使用 Qt 智能指针

Qt: How to use Qt's Smartpointers

我有 "old-fashioned" C++ 编程经验(即我关心指针和内存管理)。不过我确实想利用现代概念。

因为我的应用程序大量使用 Qt,所以我想使用 Qt 的智能指针。然而,我对一般的智能指针及其在 Qt 中的使用有些困惑。

1.) 据我所知,如果我从 QObject 派生,我最好坚持使用 Qt 的对象树和所有权模型,而忘掉智能指针。正确吗?

2.) 在 C++ 中,我可以使用 std::shared_ptrstd::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.) 特别是:我应该将函数签名更改为使用智能指针吗?它似乎创建了相当复杂的类型签名(return 类型和传递的参数)。另外,如果某些 "legacy" 函数调用另一个函数怎么办 - 用原始指针编写函数签名并仅使用智能指针 "inside" 函数会更好吗?

5.) 函数foobar中的ls应该用什么智能指针代替? mi 应该使用什么指针类型,即存储在 QList 中的对象?

您几乎被迫使用 Qt 拥有 GUI 对象原始指针的习惯用法,因为 QWidget 派生类型将承担子元素的所有权。

在其他地方,你应该尽可能避免使用任何类型的指针。大多数时候你可以传递一个引用。如果您需要多态所有权,请使用 std::unique_ptr。在 非常罕见的 情况下,您有多个独立的生命周期需要共享资源的所有权,为此您使用 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
}

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?

QSharedPointerQScopedPointer,有点类似于unique_ptr,但不支持移动语义。恕我直言,现在没有理由使用这些,只需使用标准库提供的智能指针(管理不是从 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?

通常 - 仅使用智能指针来管理内存 = 仅在存在所有权关系时使用它们。如果您只是将实例传递给与其一起工作但不取得所有权的函数,则只传递一个普通的旧指针。

关于你的例子,没有上下文很难说。 lsmi 都可以是 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?

最好说 "it depends"。首先,你应该知道什么是thread affinity in Qt is. Good example - QThread worker.

QObject 使用 parent-child 内存管理,所以如果父对象被销毁,那么它的所有子对象也将被销毁。在"Qt way"中,只管理根对象生命周期就足够了。当您在堆上创建基于 QObject 的 类 时(例如,使用 new 运算符),这非常容易。但也有一些例外:

  • 你应该小心对象 that are created on the stack
  • 类 和 parent-child 关系应该属于同一个线程。您可以使用 QObject::moveToThread() 方法来控制线程关联。所以如果相关对象的实例应该属于不同的线程,它们可能没有 parent-child 关系。

    属于不同线程的 auto-delete 个对象存在一种模式。例如,如果我们应该在 p1 被销毁时删除 p2

    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()。它尽快安排对象删除。它可能在几种情况下使用:

  • 您需要从插槽中或在发出信号期间删除一个对象。您不应直接删除此类对象,因为这可能会导致问题。示例:self-destroying 按钮(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-based 指针,有一个 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() 信号来管理内部引用,因此使用它来保存大量对象可能会导致性能问题(仅在质量 creation/destruction 上) .访问此指针的性能与原始指针相同。

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

是的,还有QSharedPointer and QScopedPointer that work in similar way. Some benefits for using these pointers include custom deleters,比如QScopedPointerDeleterQScopedPointerArrayDeleterQScopedPointerPodDeleterQScopedPointerDeleteLater

也可以这样使用,对于QObject-based 类,如果需要延期删除:

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 的 类 使用 parent-child。
  2. 仅当您无法控制对象生命周期时才使用共享指针
  3. 在所有其他情况下 - 使用唯一指针
  4. 要在两个 类 之间分享一些东西 - 使用 references
  5. 如果逻辑复杂 - 使用智能指针。但是 double-check for 循环(大多数情况下使用弱指针)

欢迎在评论中提出任何说明。

首先,现代 C++ 允许您使用值,而 Qt 支持这一点。因此,默认应该使用 QObjects,就好像它们是 non-movable、non-copyable 值一样。实际上,您的代码段根本不需要显式内存管理。这与 object 有一个 parent.

的事实完全不冲突
#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++有严格的object销毁顺序,保证objects先于other销毁。因此,到 other.~QObject() 运行时,没有 MyObject children,因此 double-deletion.

没有问题

一般来说,以下是存储 QObject 集合的可行方法及其要求:

  1. std::array - 所有相同类型的元素,固定大小,无法返回

  2. std::list - 所有相同类型的元素,没有 RandomAccessIterator,容器拥有 objects

  3. std::deque - 所有相同类型的元素,有 RandomAccessIterator 但不允许 erase 因为你的 object 不是 MoveAssignable(但当然可以是cleared/destroyed),容器拥有objects

  4. std::vector<std::unique_ptr<BaseClass>> - 任何类型的元素,即它是一个多态容器,容器拥有 objects

  5. std::vector<QObject*>QObjectList - non-owning、non-tracking 容器。 Qt代码全是QObjectList = QList<QObject*>.

  6. QObject(原文如此!) - 任何 QObject 类型的元素,容器可选择拥有 objects,容器跟踪 object 生命周期,元素指针可用于从容器中删除 object;只有一个容器可以容纳给定的 object,使用裸向量来存储 object,因此 children 的 additions/removals 是 O(N).

当存储 object 本身而不是它们的集合时,object 生命周期与包含范围相同,将它们保存为值是最简单的。例如:

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 的 object 树和所有权模型吗? 在这件事上别无选择:那个模型就在那里,它可以被禁用。但是它被设计成catch-all,你可以抢占它。 QObject 所有权模型仅确保 children 不会超过 parent。它可以防止资源泄漏。你可以在 parent 死前结束 children 的生命。你也可以自由 parentless objects.

  2. Qt中等价的智能指针有哪些?没关系。您正在编写 C++ - 使用 std::shared_ptrstd::unique_ptr。自 Qt 5.7 以来,使用 Qt 的等价物没有任何好处:从该版本开始,Qt 要求编译器支持 C++11,因此必须支持这些指针。

  3. 如何将上面的代码片段翻译成使用智能指针的版本?没有必要。按值保留 objects。也许您需要一个不同的片段,实际上需要使用智能指针。

  4. 我应该将函数签名更改为使用智能指针吗?不,您没有动机使用任何智能指针。

  5. N/A