Qt信号lambda导致shared_ptr泄漏?

pau*_*ulm 5 c++ lambda qt shared-ptr qt-signals

我有以下代码:

#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;
}
Run Code Online (Sandbox Code Playgroud)

哪个输出:

DocumentRepository
+Gui
Document
Document
Document
-Gui
~Gui
~Document
~Document
~Document
~DocumentRepository
Run Code Online (Sandbox Code Playgroud)

但是在这里你可以看到Document实例在Gui实例之后被破坏了.如果你看看你会看到的评论,我将这个问题缩小到信号的lambda使用shared_ptr.我想知道为什么会导致泄漏,如何解决?

作为参考,不使用shared_ptrlambda 时的"正确"/非泄漏输出是:

DocumentRepository
+Gui
Document
Document
Document
~Document
~Document
~Document
-Gui
~Gui
~DocumentRepository
Run Code Online (Sandbox Code Playgroud)

lpa*_*app 5

这是一个有趣的问题,让我们揭开它的神秘面纱:

官方连接文档:

如果发件人被销毁,连接将自动断开连接.但是,您应该注意,在发出信号时,仿函数中使用的任何对象仍然存在.

在您的示例中,您正在复制在lambda中使用时创建的共享指针,否则不会为共享指针创建副本.副本自然会增加共享指针内对象的引用计数器.以下是shared_ptr的相应文档:

只能通过复制构造或复制将其值分配给另一个shared_ptr,才能与另一个shared_ptr共享对象的所有权

现在,让我们区分这两种情况:

  • 当你不复制共享指针时,只有一个对象的引用,所以当你的文档存储库完成清除时,就没有更多的引用,因此你可以破坏对象,因为你没有在lambda函数中做任何有用的事情,因此可以进行优化.

  • 复制共享指针时,对lambad外部的对象有一个引用,由于共享指针复制,也会有一个引用.现在,Qt连接语义确保按照上述文档保持对象处于活动状态.

因此,当您的Gui对象被破坏时,它也将完成所有断开连接,并且在此期间,它可以确保不再有对该对象的引用,因此在您的gui析构函数print语句之后调用析构函数.

你可以在这里添加一个额外的print语句来改进测试代码:

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

这也将明确指出,在您创建存储库之后,文档对象在存储库清除后被销毁,而不是在方法终止时.输出将使其更清晰:

main.pro

TEMPLATE = app
TARGET = main
QT += widgets
CONFIG += c++11
SOURCES += main.cpp
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

产量

DocumentRepository
+Gui
Document
Document
Document
+/-Gui
~Document
~Document
~Document
-Gui
~Gui
~DocumentRepository
Run Code Online (Sandbox Code Playgroud)