Qt:如何使用Qt的Smartpointers

时间:2018-04-04 22:11:26

标签: c++ qt c++11 smart-pointers

我有过老式的经历" 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.。)特别是:我应该将功能签名更改为使用智能指针吗?它似乎创建了非常复杂的类型签名(返回类型和传递的参数)。如果一些&#34;遗产&#34;函数调用另一个函数 - 使用原始指针编写函数签名更好,并且仅使用智能指针&#34;内部&#34;功能

5.。)什么smartpointer应该替换ls函数中的foobar?应该用于mi的指针类型是什么,即QList中存储的对象?

4 个答案:

答案 0 :(得分:3)

你几乎被迫使用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
}

答案 1 :(得分:3)

首先,现代C ++允许您使用值,Qt支持它。因此,默认值应该是使用QObject s,就像它们是不可移动的,不可复制的值一样。实际上,您的代码片段根本不需要明确的内存管理。并且这与对象具有父级的事实完全没有冲突。

#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个孩子,因此双重删除没有问题。

通常,以下是存储std::array集合的可行方法及其要求:

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

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

  3. RandomAccessIterator - 所有相同类型的元素都有erase但不允许MoveAssignable,因为您的对象不是std::vector<std::unique_ptr<BaseClass>>(但当然可以清除/销毁),容器拥有对象

  4. std::vector<QObject*> - 任何类型的元素,即它是一个多态容器,容器拥有对象

  5. QObjectListQObjectList = QList<QObject*> - 非拥有,非跟踪容器。 Qt代码已满QObject

  6. QObject(sic!) - 任何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 ++; } } };

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

    QObject

    现在,回答你的问题:

    1. 我应该更好地坚持Qt的对象树和所有权模型吗?在这个问题上别无选择:那个模型在那里,不能被禁用。但它被设计成一个包罗万象,你可以抢占它。 std::shared_ptr所有权模型仅确保孩子不会超过父母。它可以防止资源泄漏。在父母去世之前,你可以自由地结束孩子的生命。您也可以自由拥有无父对象。

    2. Qt中的等效智能指针是什么?没关系。您正在编写C ++ - 使用std::unique_ptrconst。从Qt 5.7开始,使用Qt的等价物没有任何好处:从那个版本开始,Qt需要在编译器中支持C ++ 11,因此必须支持这些指针。

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

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

    5. N / A

答案 2 :(得分:2)

  

1。)据我了解,如果我从QObject派生,我应该更好地坚持Qt的对象树和所有权模型而忘记   smartpointers。正确的吗?

是。使用QObjects时,我建议依靠它的父子模型来管理内存。它运作良好,你无法完全避免它,所以使用它。

  

2。)在C ++中,我可以使用std :: shared_ptr和std :: unique_ptr。 Qt中等效的智能指针是什么?

QSharedPointerQScopedPointer,它与unique_ptr有些相似,但不支持移动语义。恕我直言,现在没有理由使用这些,只需使用标准库提供的智能指针(管理不是从QObject派生的对象的生命周期。)

  

4.。)特别是:我应该将功能签名更改为使用智能指针吗?它似乎创建了非常复杂的类型签名   (返回类型和传递的参数)。还有什么,如果一些“遗产”   函数调用另一个函数 - 写函数是否更好   带有原始指针的签名,并仅在“内部”使用smartpointers   功能

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

关于你的例子,没有上下文就很难说。 lsmi都可以是unique_ptr。更好的是,您可以在堆栈上分配ls。更好的是,避免使用QList并使用std::vector或类似的东西。

答案 3 :(得分:2)

  
      
  1. 据我了解,如果我从QObject派生,我最好坚持使用Qt的对象树和所有权模型而忘记   smartpointers。正确的吗?
  2.   

更好地说&#34;它取决于&#34;。首先,您应该知道thread affinity in Qt是什么。很好的例子 - QThread worker

QObject使用父子内存管理,因此如果父项被销毁,那么它的所有子项也将被销毁。在&#34; Qt方式&#34;中,仅管理根对象生存期就足够了。在堆上创建基于QObject的类时(例如,使用new运算符),这非常简单。但也有一些例外:

  • 您应该小心对象that are created on the stack
  • 具有父子关系的类应属于同一个线程。您可以使用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. 在C ++中,我可以使用std::shared_ptrstd::unique_ptr。 Qt中等效的智能指针是什么?
  2.   

是的,QSharedPointerQScopedPointer的工作方式类似。使用这些指针的一些好处包括custom deleters,例如QScopedPointerDeleterQScopedPointerArrayDeleterQScopedPointerPodDeleterQScopedPointerDeleteLater

如果您需要推迟删除,也可以使用类似这样的内容,基于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. 要在两个班级之间分享内容 - 请使用references
  5. 如果是复杂的逻辑 - 使用智能指针。但要仔细检查 for循环(在大多数情况下使用弱指针)
  6. 随意在评论中提出任何澄清。