Qt:如何使用 Qt 智能指针
Qt: How to use Qt's Smartpointers
我有 "old-fashioned" C++ 编程经验(即我关心指针和内存管理)。不过我确实想利用现代概念。
因为我的应用程序大量使用 Qt,所以我想使用 Qt 的智能指针。然而,我对一般的智能指针及其在 Qt 中的使用有些困惑。
1.) 据我所知,如果我从 QObject
派生,我最好坚持使用 Qt 的对象树和所有权模型,而忘掉智能指针。正确吗?
2.) 在 C++ 中,我可以使用 std::shared_ptr
和 std::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.) 特别是:我应该将函数签名更改为使用智能指针吗?它似乎创建了相当复杂的类型签名(return 类型和传递的参数)。另外,如果某些 "legacy" 函数调用另一个函数怎么办 - 用原始指针编写函数签名并仅使用智能指针 "inside" 函数会更好吗?
5.) 函数foobar
中的ls
应该用什么智能指针代替? mi
应该使用什么指针类型,即存储在 QList
中的对象?
您几乎被迫使用 Qt
拥有 GUI 对象原始指针的习惯用法,因为 QWidget
派生类型将承担子元素的所有权。
在其他地方,你应该尽可能避免使用任何类型的指针。大多数时候你可以传递一个引用。如果您需要多态所有权,请使用 std::unique_ptr
。在 非常罕见的 情况下,您有多个独立的生命周期需要共享资源的所有权,为此您使用 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.) As far as I understand, if I derive from QObject, I should better stick to Qt's object tree and ownership model and forget about
smartpointers. Correct?
是的。使用 QObjects
时,我建议依靠它的父子模型来管理内存。它效果很好,你不能完全避免它,所以使用它。
2.) In C++ I can get by with std::shared_ptr and std::unique_ptr. What are the equivalent smart pointers in Qt?
有QSharedPointer
和QScopedPointer
,有点类似于unique_ptr
,但不支持移动语义。恕我直言,现在没有理由使用这些,只需使用标准库提供的智能指针(管理不是从 QObject
派生的对象的生命周期)。
4.) In particular: Should I change function signature into using smartpointers? It seems to create quite complex type signatures
(return type and passed arguments). Also what if some "legacy"
function calls another function - is it better to write function
signatures with raw pointers, and use smartpointers only "inside"
functions?
通常 - 仅使用智能指针来管理内存 = 仅在存在所有权关系时使用它们。如果您只是将实例传递给与其一起工作但不取得所有权的函数,则只传递一个普通的旧指针。
关于你的例子,没有上下文很难说。 ls
和 mi
都可以是 unique_ptr
。更好的是,您可以只在堆栈上分配 ls
。更好的是,避免使用 QList
并使用 std::vector
或类似的东西。
- As far as I understand, if I derive from QObject, I should better stick to Qt's object tree and ownership model and forget about
smartpointers. Correct?
最好说 "it depends"。首先,你应该知道什么是thread affinity in Qt is. Good example - QThread worker
.
QObject
使用 parent-child 内存管理,所以如果父对象被销毁,那么它的所有子对象也将被销毁。在"Qt way"中,只管理根对象生命周期就足够了。当您在堆上创建基于 QObject
的 类 时(例如,使用 new
运算符),这非常容易。但也有一些例外:
- 你应该小心对象 that are created on the stack。
类 和 parent-child 关系应该属于同一个线程。您可以使用 QObject::moveToThread()
方法来控制线程关联。所以如果相关对象的实例应该属于不同的线程,它们可能没有 parent-child 关系。
属于不同线程的 auto-delete 个对象存在一种模式。例如,如果我们应该在 p1
被销毁时删除 p2
:
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()
。它尽快安排对象删除。它可能在几种情况下使用:
您需要从插槽中或在发出信号期间删除一个对象。您不应直接删除此类对象,因为这可能会导致问题。示例:self-destroying 按钮(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-based 指针,有一个 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()
信号来管理内部引用,因此使用它来保存大量对象可能会导致性能问题(仅在质量 creation/destruction 上) .访问此指针的性能与原始指针相同。
- In C++ I can get by with
std::shared_ptr
and std::unique_ptr
. What are the equivalent smart pointers in Qt?
是的,还有QSharedPointer
and QScopedPointer
that work in similar way. Some benefits for using these pointers include custom deleters,比如QScopedPointerDeleter
,QScopedPointerArrayDeleter
,QScopedPointerPodDeleter
,QScopedPointerDeleteLater
。
也可以这样使用,对于QObject-based 类,如果需要延期删除:
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++ 代码风格。个人做法,以我的经验:
- 仅对基于
QWidget
的 类 使用 parent-child。
- 仅当您无法控制对象生命周期时才使用共享指针
- 在所有其他情况下 - 使用唯一指针
- 要在两个 类 之间分享一些东西 - 使用 references。
- 如果逻辑复杂 - 使用智能指针。但是 double-check
for 循环(大多数情况下使用弱指针)
欢迎在评论中提出任何说明。
首先,现代 C++ 允许您使用值,而 Qt 支持这一点。因此,默认应该使用 QObject
s,就好像它们是 non-movable、non-copyable 值一样。实际上,您的代码段根本不需要显式内存管理。这与 object 有一个 parent.
的事实完全不冲突
#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++有严格的object销毁顺序,保证objects
先于other
销毁。因此,到 other.~QObject()
运行时,没有 MyObject
children,因此 double-deletion.
没有问题
一般来说,以下是存储 QObject
集合的可行方法及其要求:
std::array
- 所有相同类型的元素,固定大小,无法返回
std::list
- 所有相同类型的元素,没有 RandomAccessIterator
,容器拥有 objects
std::deque
- 所有相同类型的元素,有 RandomAccessIterator
但不允许 erase
因为你的 object 不是 MoveAssignable
(但当然可以是cleared/destroyed),容器拥有objects
std::vector<std::unique_ptr<BaseClass>>
- 任何类型的元素,即它是一个多态容器,容器拥有 objects
std::vector<QObject*>
或 QObjectList
- non-owning、non-tracking 容器。 Qt代码全是QObjectList = QList<QObject*>
.
QObject
(原文如此!) - 任何 QObject
类型的元素,容器可选择拥有 objects,容器跟踪 object 生命周期,元素指针可用于从容器中删除 object;只有一个容器可以容纳给定的 object,使用裸向量来存储 object,因此 children 的 additions/removals 是 O(N)
.
当存储 object 本身而不是它们的集合时,object 生命周期与包含范围相同,将它们保存为值是最简单的。例如:
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 ++;
}
}
};
现在,回答您的问题:
我应该更好地坚持 Qt 的 object 树和所有权模型吗? 在这件事上别无选择:那个模型就在那里,它可以被禁用。但是它被设计成catch-all,你可以抢占它。 QObject
所有权模型仅确保 children 不会超过 parent。它可以防止资源泄漏。你可以在 parent 死前结束 children 的生命。你也可以自由 parentless objects.
Qt中等价的智能指针有哪些?没关系。您正在编写 C++ - 使用 std::shared_ptr
和 std::unique_ptr
。自 Qt 5.7 以来,使用 Qt 的等价物没有任何好处:从该版本开始,Qt 要求编译器支持 C++11,因此必须支持这些指针。
如何将上面的代码片段翻译成使用智能指针的版本?没有必要。按值保留 objects。也许您需要一个不同的片段,实际上需要使用智能指针。
我应该将函数签名更改为使用智能指针吗?不,您没有动机使用任何智能指针。
N/A
我有 "old-fashioned" C++ 编程经验(即我关心指针和内存管理)。不过我确实想利用现代概念。
因为我的应用程序大量使用 Qt,所以我想使用 Qt 的智能指针。然而,我对一般的智能指针及其在 Qt 中的使用有些困惑。
1.) 据我所知,如果我从 QObject
派生,我最好坚持使用 Qt 的对象树和所有权模型,而忘掉智能指针。正确吗?
2.) 在 C++ 中,我可以使用 std::shared_ptr
和 std::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.) 特别是:我应该将函数签名更改为使用智能指针吗?它似乎创建了相当复杂的类型签名(return 类型和传递的参数)。另外,如果某些 "legacy" 函数调用另一个函数怎么办 - 用原始指针编写函数签名并仅使用智能指针 "inside" 函数会更好吗?
5.) 函数foobar
中的ls
应该用什么智能指针代替? mi
应该使用什么指针类型,即存储在 QList
中的对象?
您几乎被迫使用 Qt
拥有 GUI 对象原始指针的习惯用法,因为 QWidget
派生类型将承担子元素的所有权。
在其他地方,你应该尽可能避免使用任何类型的指针。大多数时候你可以传递一个引用。如果您需要多态所有权,请使用 std::unique_ptr
。在 非常罕见的 情况下,您有多个独立的生命周期需要共享资源的所有权,为此您使用 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.) As far as I understand, if I derive from QObject, I should better stick to Qt's object tree and ownership model and forget about smartpointers. Correct?
是的。使用 QObjects
时,我建议依靠它的父子模型来管理内存。它效果很好,你不能完全避免它,所以使用它。
2.) In C++ I can get by with std::shared_ptr and std::unique_ptr. What are the equivalent smart pointers in Qt?
有QSharedPointer
和QScopedPointer
,有点类似于unique_ptr
,但不支持移动语义。恕我直言,现在没有理由使用这些,只需使用标准库提供的智能指针(管理不是从 QObject
派生的对象的生命周期)。
4.) In particular: Should I change function signature into using smartpointers? It seems to create quite complex type signatures (return type and passed arguments). Also what if some "legacy" function calls another function - is it better to write function signatures with raw pointers, and use smartpointers only "inside" functions?
通常 - 仅使用智能指针来管理内存 = 仅在存在所有权关系时使用它们。如果您只是将实例传递给与其一起工作但不取得所有权的函数,则只传递一个普通的旧指针。
关于你的例子,没有上下文很难说。 ls
和 mi
都可以是 unique_ptr
。更好的是,您可以只在堆栈上分配 ls
。更好的是,避免使用 QList
并使用 std::vector
或类似的东西。
- As far as I understand, if I derive from QObject, I should better stick to Qt's object tree and ownership model and forget about smartpointers. Correct?
最好说 "it depends"。首先,你应该知道什么是thread affinity in Qt is. Good example - QThread worker
.
QObject
使用 parent-child 内存管理,所以如果父对象被销毁,那么它的所有子对象也将被销毁。在"Qt way"中,只管理根对象生命周期就足够了。当您在堆上创建基于 QObject
的 类 时(例如,使用 new
运算符),这非常容易。但也有一些例外:
- 你应该小心对象 that are created on the stack。
类 和 parent-child 关系应该属于同一个线程。您可以使用
QObject::moveToThread()
方法来控制线程关联。所以如果相关对象的实例应该属于不同的线程,它们可能没有 parent-child 关系。属于不同线程的 auto-delete 个对象存在一种模式。例如,如果我们应该在
p1
被销毁时删除p2
: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()
。它尽快安排对象删除。它可能在几种情况下使用:您需要从插槽中或在发出信号期间删除一个对象。您不应直接删除此类对象,因为这可能会导致问题。示例:self-destroying 按钮(
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-based 指针,有一个 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()
信号来管理内部引用,因此使用它来保存大量对象可能会导致性能问题(仅在质量 creation/destruction 上) .访问此指针的性能与原始指针相同。
- In C++ I can get by with
std::shared_ptr
andstd::unique_ptr
. What are the equivalent smart pointers in Qt?
是的,还有QSharedPointer
and QScopedPointer
that work in similar way. Some benefits for using these pointers include custom deleters,比如QScopedPointerDeleter
,QScopedPointerArrayDeleter
,QScopedPointerPodDeleter
,QScopedPointerDeleteLater
。
也可以这样使用,对于QObject-based 类,如果需要延期删除:
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++ 代码风格。个人做法,以我的经验:
- 仅对基于
QWidget
的 类 使用 parent-child。 - 仅当您无法控制对象生命周期时才使用共享指针
- 在所有其他情况下 - 使用唯一指针
- 要在两个 类 之间分享一些东西 - 使用 references。
- 如果逻辑复杂 - 使用智能指针。但是 double-check for 循环(大多数情况下使用弱指针)
欢迎在评论中提出任何说明。
首先,现代 C++ 允许您使用值,而 Qt 支持这一点。因此,默认应该使用 QObject
s,就好像它们是 non-movable、non-copyable 值一样。实际上,您的代码段根本不需要显式内存管理。这与 object 有一个 parent.
#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++有严格的object销毁顺序,保证objects
先于other
销毁。因此,到 other.~QObject()
运行时,没有 MyObject
children,因此 double-deletion.
一般来说,以下是存储 QObject
集合的可行方法及其要求:
std::array
- 所有相同类型的元素,固定大小,无法返回std::list
- 所有相同类型的元素,没有RandomAccessIterator
,容器拥有 objectsstd::deque
- 所有相同类型的元素,有RandomAccessIterator
但不允许erase
因为你的 object 不是MoveAssignable
(但当然可以是cleared/destroyed),容器拥有objectsstd::vector<std::unique_ptr<BaseClass>>
- 任何类型的元素,即它是一个多态容器,容器拥有 objectsstd::vector<QObject*>
或QObjectList
- non-owning、non-tracking 容器。 Qt代码全是QObjectList = QList<QObject*>
.QObject
(原文如此!) - 任何QObject
类型的元素,容器可选择拥有 objects,容器跟踪 object 生命周期,元素指针可用于从容器中删除 object;只有一个容器可以容纳给定的 object,使用裸向量来存储 object,因此 children 的 additions/removals 是O(N)
.
当存储 object 本身而不是它们的集合时,object 生命周期与包含范围相同,将它们保存为值是最简单的。例如:
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 ++;
}
}
};
现在,回答您的问题:
我应该更好地坚持 Qt 的 object 树和所有权模型吗? 在这件事上别无选择:那个模型就在那里,它可以被禁用。但是它被设计成catch-all,你可以抢占它。
QObject
所有权模型仅确保 children 不会超过 parent。它可以防止资源泄漏。你可以在 parent 死前结束 children 的生命。你也可以自由 parentless objects.Qt中等价的智能指针有哪些?没关系。您正在编写 C++ - 使用
std::shared_ptr
和std::unique_ptr
。自 Qt 5.7 以来,使用 Qt 的等价物没有任何好处:从该版本开始,Qt 要求编译器支持 C++11,因此必须支持这些指针。如何将上面的代码片段翻译成使用智能指针的版本?没有必要。按值保留 objects。也许您需要一个不同的片段,实际上需要使用智能指针。
我应该将函数签名更改为使用智能指针吗?不,您没有动机使用任何智能指针。
N/A