我有以下代码:
#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
答案 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");
这也将明确地向您表明文档对象在存储库清除后被破坏,而不是在创建它们时终止方法。输出将使其更清晰:
TEMPLATE = app
TARGET = main
QT += widgets
CONFIG += c++11
SOURCES += 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保持活动状态。