[Development] The future of smart pointers in Qt API

Volker Hilsheimer volker.hilsheimer at qt.io
Mon Feb 3 15:22:37 CET 2020


> On 1 Feb 2020, at 19:20, Daniel Teske <qt at squorn.de> wrote:
>> Parent-child relationship is not going to go away, it has purpose beyond object lifetime: the visual hierarchy is encoded in the parent/child relationship of widgets, for example.
>> 
>> Making ownership of objects, and assumptions regarding their life-cycle, explicit in code is a good thing though. Using smart pointers is the idiomatic C++ way to do so. But it doens’t have to be the only way; a call to QObject::setParent is IMHO just as explicit, and parent/child in Qt is a concept you have to learn either way.
>> 
>> But sometimes our APIs are messy. QTabWidget::addTab transfers ownership of the QWidget*; QTabWidget::addAction does not transfer ownership of the QAction*. Unless you understand fairly well how QAction works, or unless you read/remember the documentation, it’s impossible to see from the code what happens wrt to object ownership.
>> 
>> API naming is not a scalable way to make the distinction, so finding how we can use smart pointers to make it clear in the code when ownership is transferred is a good idea.
>> 
>> Perhaps the pragmatic approach is to address this in those APIs that take a QObject* but don’t transfer ownership. I believe those are much fewer. Otherwise, the convention can be that any API that takes a raw pointer takes ownership. How this is implemented internally doesn’t matter.
>> 
>> 
> 
> I have already invested quite a bit of time and thinking into how to add unique_ptr apis to Qt. And I encourage everyone to go back and read my previous mails, and/or the actual patch I produced.
> 
> Because I have already presented a design that is:
> 
> * Backwards compatible, existing code continues to work.
> * And adds a lot of memory safety to Qt. Though not without a loop-hole.
> * And actually exists as a experimental patch, not just some abstract idea.
> 
> To explain it once more, the key insight is the following invariant:
> 
> A QObject[1] is either owned by its parent, or by a unique_ptr[2].
> 
> [1]: Some classes use the same parent-child concept as QObject. 
> [2]: Stack allocated objects are essentially equivalent to unique_ptr held ones.
> 
> That implies that a unique_ptr owned QObject should have no parent.
> 
> That invariant ensures that there's exactly one owner for each object, and thus ensures that every object is deleted. 
> 
> But it doesn't guarantee that the owner is what the user expected. 
> 
> So to take your examples:
> 
> QTabWidget::addAction: This never transfers ownership, thus the invariant holds.
> 
> parent->addTab(tab): This has three cases:
> 
> * tab is owned already by parent: Invariant holds
> * tab is owned by a different parent: Invariant holds
> 
> And the last case, and that's the small loop hole in the design:
> * tab has no parent
> 
> That means, the child is owned by a unique_ptr, and the user must have called .get() on it. 
> 
> Thus the small case where the user has to be cautious is when they have a raw pointer for a object owned by a unique_ptr. Luckily with the design I have this is should be rare. 
> 
> In addition to the existing QTabWidget::addTab, the patch then adds:
> 
> QTabWidget::addTab(unique_ptr<>) for which the invariant obviously holds.
> 
> Now to take your next example: setParent is not fine. setParent can be used in 4 different ways:
> 
> child->setParent(child->parent()): Invariant holds
> child->setParent(newParent): Invariant holds
> child->setParent(nullptr): Nope
> orphan->setParent(parent): Nope
> 
> The solution is I have is to replace setParent with two functions: adoptChild + releaseChild, which both ensure that the invariant always holds.
> 
> And to deprecate setParent. I actually made that deprecation opt-in, because of the next part:
> 
> Construction of QObjects:
> 
> The goal is to enable modern C++, meaning that users can choose to never use "new". 
> 
> The first problem is then that a ctor such as this:
> 
> QTabWidget(QWidget *parent = nullptr)
> 
> can be used via make_unique:
> 
> auto child = make_unique<QTabWidget>(someParent);
> 
> That call would then violate the invariant, and thus in my patch I split up the ctor of QTabWidget into 2:
> 
> QTabWidget()
> QTabWidget(QWidget *parent)
> 
> This is obviously backwards compatible. 
> The first ctor can be used without violating the invariant, whereas the second one violates it. And thus it's again optionally deprecated.


Hi Daniel,

I like many things in this proposal, especially the principles; thanks for that!

Where I’m on the fence is the replacing of setParent with “adoptChild”. I think of “parent” as a property of an object, so setParent/parent accessors are the API that fits into my mental model. I have a hard time thinking about “list of children” as that property..

But I think the example below shows exactly the problem <cliff_hanger>

> Now for some classes the parent ptr has additional meaning beyond just being the owner. E.g. for the layout classes. For that reason there's a bit of infrastructure, which makes this work:
> 
> parent->makeChild<QTabWidget>();
> 
> or to take a more complex example:
> 
> parent->makeChild<QLabel>("Hello World!");
> 
> (The infrastructure is a third constructor, and a forwarding function defined via Q_OBJECT)
> 
> For example, converting some random code on https://doc.qt.io/qt-5/qtwidgets-widgets-lineedits-example.html to use makeChild would look like this:
> 
> Window::Window(QWidget
>  *parent)
>     : 
> QWidget
> (parent)
> {
>   QGroupBox *echoGroup = makeChild<QGroupBox>(tr("Echo"));
>   QLabel *echoLabel = 
> echoGroup
> ->makeChild<QLabel>(tr("Mode"));
>   QComboBox *comboBox = 
> echoGroup
> ->makeChild<QComboBox>();
>   [...]
>   QGridLayout *echoLayout = echoGroup->makeChild<QGridLayout>();
> 
>   echoLayout->addWidget(echoLabel, 0, 0);
> 
>   echoLayout
> ->addWidget(echoComboBox, 0, 1);
> 
>   echoLayout
> ->addWidget(echoLineEdit, 1, 0, 1, 2);
> 


This works, but now you are encoding the visual hierarchy of the widgets in two places: when asking the presumed parent to create the widgets; and when setting up the layout, which might do things differently (esp once you start refactoring complex UIs).



> Or if using unique_ptrs:
> 
> Window::Window(QWidget
>  *parent)
>     : 
> QWidget
> (parent)
> {
>   auto echoGroup = std::make_unique<QGroupBox>(tr("Echo"));
>   auto echoLabel = std::make_unique<QLabel>(tr("Mode"));
>   auto comboBox = std::make_unique<QComboBox>();
>   [...]
>   auto echoLayout = std::make_unique<QGridLayout>();
> 
>   echoLayout->addWidget(std::move(echoLabel), 0, 0);
> 
>   echoLayout
> ->addWidget(std::move(echoComboBox), 0, 1);
> 
>   echoLayout
> ->addWidget(std::move(echoLineEdit), 1, 0, 1, 2);


I don’t think these lines above are correct.

QBoxLayout::addWidget does *not* take ownership. Widgets are reparented to their correct parent once you call QWidget::setLayout in the next line, and QLayout operates on QLayoutItems that hold a weak pointer to the widgets that the layout places.


>   echoGroup
> ->setLayout(std::move(echoLayout));
> And obviously the use can choose whatever they like, though I would recommend the first variant. 
> 
> Imho this is the nicest aspect of this design. Adding lots of unique_ptr apis lead to no usage of unique_ptrs. Instead, there's just a clearer way to construct the usual parent-child relationships.
> 
> There are some details I'm omitting here, and some apis that needs more work but that's the core idea.



So, how do we get a weak “observer" pointer into the layout, and reset the unique_ptrs only in the setLayout line when the widgets are de-facto no longer owned by the unique_ptr?


Cheers,
Volker




More information about the Development mailing list