Qt信号lambda导致shared_ptr泄漏?

时间:2014-10-26 16:04:25

标签: c++ qt lambda 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;
}

哪个输出:

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

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

作为参考,在lambda中不使用shared_ptr时,“正确”/非泄漏输出是:

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

2 个答案:

答案 0 :(得分:5)

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

来自official connect documentation

  

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

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

  

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

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

  • 当您不复制共享指针时,只有一个对象的引用,因此当对文档存储库进行清除时,不再有对它的引用,因此对象可能被破坏鉴于你没有在lambda函数中做任何有用的事情,因此可以进行优化。

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

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

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

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

这也将明确地向您表明文档对象在存储库清除后被破坏,而不是在创建它们时终止方法。输出将使其更清晰:

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;
}

输出

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

答案 1 :(得分:3)

Lambda函数,即使它们感觉很神奇,也基本上是正常的函子,其中捕获的变量存储在成员变量中。当您实例化lambda函数并将其销毁时会复制它们。

lambda中存储了std::shared_ptr<Document>,通过[=]复制,因为你在lambda的主体中引用它,完整的lambda本身被复制到Qt连接以及这个shared_ptr。

所以它在技术上不是泄漏,你只是通过额外的shared_ptr实例持有一个引用,直到lambda被销毁,这发生在第一个连接发射器或接收器被销毁时(在你的情况下是一个Gui对象)。

由于连接绑定到Document对象,因此确保lambda仅捕获正常指针或引用将避免使Document保持活动状态。