Lambda Qt cause shared_ptr leak?

I have the following code:

#include <QApplication> #include <memory> #include <QUndoCommand> #include <QWidget> class Document { public: Document() { qDebug("Document"); } ~Document() { qDebug("~Document"); } QUndoStack mUndostack; }; class DocumentRepository { public: DocumentRepository() { qDebug("DocumentRepository"); } ~DocumentRepository() { qDebug("~DocumentRepository"); } void AddDoc(std::shared_ptr<Document> doc) { mDocs.emplace_back(doc); } std::vector<std::shared_ptr<Document>> mDocs; }; class Gui : public QWidget { public: Gui(DocumentRepository& repo) : mRepo(repo) { qDebug("+Gui"); for(int i=0; i<3; i++) { CreateDoc(); } mRepo.mDocs.clear(); qDebug("-Gui"); } ~Gui() { qDebug("~Gui"); } void CreateDoc() { auto docPtr = std::make_shared<Document>(); connect(&docPtr->mUndostack, &QUndoStack::cleanChanged, this, [=](bool clean) { // Using docPtr here causes a memory leak on the shared_ptr's, the destruct after ~Gui // but without using docPtr here they destruct before ~Gui as exepected. QString msg = "cleanChanged doc undo count " + QString::number(docPtr->mUndostack.count()); qDebug(msg.toLatin1()); }, Qt::QueuedConnection); mRepo.AddDoc(docPtr); } DocumentRepository& mRepo; }; int main(int argc, char *argv[]) { QApplication a(argc, argv); DocumentRepository repo; Gui g(repo); g.show(); return 0; } 

What outputs:

 DocumentRepository +Gui Document Document Document -Gui ~Gui ~Document ~Document ~Document ~DocumentRepository 

But here you can see that Document instances leak as they break after the Gui instance. If you look at the comments, you will see that I narrowed this problem down to a lambda signal using shared_ptr . I want to know why this causes a leak and how can it be solved?

For reference, the "correct" / non-current output if shared_ptr is not used in lambda:

 DocumentRepository +Gui Document Document Document ~Document ~Document ~Document -Gui ~Gui ~DocumentRepository 
+5
source share
2 answers

This is an interesting question, let's demystify it:

From the official documentation:

The connection will be automatically disconnected if the sender is destroyed. However, you must ensure that all objects used in the functor are still alive when they emit a signal.

In your example, you are copying the generic pointer created when used inside the lambda, otherwise the copy will not be made for the generic pointer. A copy will naturally increase the reference count for an object inside shared pointers. Here is the relevant documentation from shared_ptr :

Property of an object can only be transferred to another shared_ptr by copying or copying, assigning its value to another shared_ptr

Now we separate these two cases:

  • If you do not copy the common pointer, there is only one reference to the object, and therefore, when clearing is performed for your document repository, there are no more links on it, and therefore, the object can be destroyed, considering that you are not doing anything useful inside the lambda -functions and therefore can be optimized.

  • When you copy a shared pointer, there is one reference to an object outside of lambad, and inside there will also be one due to a shared copy of the pointer. Now that the semantics of the Qt connection are made, make sure that the object is supported according to the documentation above.

Therefore, when your Gui object is destroyed, it will also make the whole trip, and during this period, it can make sure that there is no longer a reference to the object and, therefore, the destructor called after your print operator gui destructor.

You can probably improve the test code by adding another print statement to it:

 qDebug("+Gui"); for(int i=0; i<3; i++) CreateDoc(); qDebug("+/-Gui"); mRepo.mDocs.clear(); qDebug("-Gui"); 

This will also explicitly indicate that you are deleting document objects after deleting the repository, and not when the method ends when they are created. The result will be more clear:

main.pro

 TEMPLATE = app TARGET = main QT += widgets CONFIG += c++11 SOURCES += main.cpp 

main.cpp

 #include <QApplication> #include <memory> #include <QUndoCommand> #include <QWidget> #include <QDebug> struct Document { Document() { qDebug("Document"); } ~Document() { qDebug("~Document"); } QUndoStack mUndostack; }; struct DocumentRepository { DocumentRepository() { qDebug("DocumentRepository"); } ~DocumentRepository() { qDebug("~DocumentRepository"); } void AddDoc(std::shared_ptr<Document> doc) { mDocs.emplace_back(doc); } std::vector<std::shared_ptr<Document>> mDocs; }; struct Gui : public QWidget { Gui(DocumentRepository& repo) : mRepo(repo) { qDebug("+Gui"); for(int i=0; i<3; i++) CreateDoc(); qDebug("+/-Gui"); mRepo.mDocs.clear(); qDebug("-Gui"); } ~Gui() { qDebug("~Gui"); } void CreateDoc() { auto docPtr = std::make_shared<Document>(); connect(&docPtr->mUndostack, &QUndoStack::cleanChanged, this, [=](bool) { /* qDebug() << docPtr->mUndostack.count(); */ }, Qt::QueuedConnection); mRepo.AddDoc(docPtr); } DocumentRepository& mRepo; }; int main(int argc, char *argv[]) { QApplication a(argc, argv); DocumentRepository repo; Gui g(repo); g.show(); return 0; } 

Exit

 DocumentRepository +Gui Document Document Document +/-Gui ~Document ~Document ~Document -Gui ~Gui ~DocumentRepository 
+5
source

Lambda functions, even if they feel magic, are basically normal functors, where the captured variables are stored in member variables. They are copied when you create the lambda function and destroy it with destruction.

The lambda stores std::shared_ptr<Document> , copied via [=] , since you refer to it in the body of the lambda, and the full copy of the lambda is copied to the Qt connection along with this shared_ptr.

So technically this is not a leak, you just keep the link through an additional instance of shared_ptr until the lambda is destroyed, which happens when the first emitter or receiver of the connection is destroyed (Gui object in your case).

Since the connection is bound to your Document object, make sure that the lambda only fixes the regular pointer, or the link will not allow you to keep the document in an active state.

+3
source

Source: https://habr.com/ru/post/1205550/


All Articles