Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Qt: How to use Qt's Smartpointers

I have experience in "old-fashioned" C++ programming (i.e. I care myself about pointers and memory management). I do want to make use of modern concepts though.

Since my application makes heavy use of Qt, I'd like to use Qt's smartpointers. I am however somewhat confused about smartpointers in general, and their use in Qt.

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?

2.) In C++ I can get by with std::shared_ptr and std::unique_ptr. What are the equivalent smart pointers in Qt?

Suppose I have the following code:

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.) How to translate the above snippet into a version using smartpointers?

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?

5.) What smartpointer should replace ls in the function foobar? What is the pointer type that should be used for mi, i.e. the objects stored in the QList?

like image 231
ndbd Avatar asked Apr 04 '18 22:04

ndbd


2 Answers

You are pretty much forced to use Qt idiom of owning raw pointers for GUI objects, as QWidget derived types will assume ownership of child elements.

Elsewhere, you should as much as possible avoid using any type of pointer. Most of the time you can pass a reference. If you need polymorphic ownership, then use std::unique_ptr. In very rare cicrcumstances, you have multiple independent lifetimes that need ownership of a shared resource, for which you use std::shared_ptr.

The Qt collection classes also interact badly with modern C++ constructs, e.g.

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*/ }

Your snippet would be

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
}
like image 108
Caleth Avatar answered Sep 17 '22 22:09

Caleth


First of all, modern C++ allows you to use values, and Qt supports that. Thus, the default should be to use QObjects as if they were non-movable, non-copyable values. Your snippet, in fact, requires no explicit memory management at all. And this does not clash at all with the fact that the objects have a 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++ has strict object destruction order, and objects is guaranteed to be destroyed before other. Thus, by the time other.~QObject() runs, there are no MyObject children, and thus there's no issue with double-deletion.

In general, here are the viable approaches to storing QObject collections, with their requirements:

  1. std::array - all elements of same type, fixed size, cannot be returned

  2. std::list - all elements of same type, doesn't have a RandomAccessIterator, container owns the objects

  3. std::deque - all elements of same type, has RandomAccessIterator but doesn't allow erase since your objects are not MoveAssignable (but of course can be cleared/destroyed), container owns the objects

  4. std::vector<std::unique_ptr<BaseClass>> - elements of any type i.e. it is a polymorphic container, container owns the objects

  5. std::vector<QObject*> or QObjectList - non-owning, non-tracking container. Qt code is full of QObjectList = QList<QObject*>.

  6. QObject (sic!) - elements of any QObject type, container optionally owns the objects, container tracks the object lifetime, the element pointer can be used to remove the object from the container; only one container can hold a given object, uses a bare vector to store the objects and thus additions/removals of children are O(N).

When storing the objects themselves, and not their collections, with object lifetime being the same as the containing scope, it's easiest to keep them as values. For example:

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

Now, to answer your questions:

  1. Should I better stick to Qt's object tree and ownership model? There's no choice in that matter: that model is there and it can't be disabled. But it's designed as a catch-all and you can preempt it. QObject ownership model only ensures that children don't outlive the parent. It prevents resource leaks. You're free to end the childrens' lives before the parent dies. You're also free to have parentless objects.

  2. What are the equivalent smart pointers in Qt? It doesn't matter. You're writing C++ - use std::shared_ptr and std::unique_ptr. There's no benefit to using Qt's equivalents since Qt 5.7: from that version onwards, Qt requires C++11 support in the compiler, and thus those pointers must be supported.

  3. How to translate the above snippet into a version using smart pointers? There's no need to. Keep objects by value. Perhaps you need to have a different snippet that would actually require the use of smart pointers.

  4. Should I change function signature into using smart pointers? No. You haven't motivated any smart pointer use.

  5. N/A

like image 28
Kuba hasn't forgotten Monica Avatar answered Sep 18 '22 22:09

Kuba hasn't forgotten Monica