QObjects, Ownership, propagate_const and C++ Evolution Const Correctness in Qt Applications
A very common implementation pattern for QObject subclasses is to declare its child QObjects as data members of type “pointer to child.” Raise your hand No, keep your hand on your computer input device 🙂 Nod if you have ever seen code like this (and maybe even written code like this yourself):
class MyWidget : public QWidget { Q_OBJECT public: explicit MyWidget(QWidget *parent = nullptr); private: DataModel *m_dataModel; QTreeView *m_view; QLineEdit *m_searchField; };
A fairly common question regarding this pattern is: “Why are we using (raw) pointers for data members”?
Of course, if we are just accessing an object that we don’t own/manage, it makes perfect sense to simply store a pointer to it (so that we’re actually able to use the object). A raw pointer is actually the Modern C++™ design here, to deliberately express the lack of ownership.
Ownership Models
The answer becomes slightly more nuanced when MyWidget actually owns the objects in question. In this case, the implementation designs are basically two:
- As shown above, use pointers to the owned objects. Qt code would typically still use raw pointers in this case, and rely on the parent/child relationship in order to manage the ownership. In other words, we will use the pointers to access the objects but not to manage their lifetime.
class MyWidget : public QWidget { Q_OBJECT public: explicit MyWidget(QWidget *parent = nullptr); private: DataModel *m_dataModel; QTreeView *m_view; QLineEdit *m_searchField; }; MyWidget::MyWidget(QWidget *parent) : QWidget(parent) { // Children parented to `this` m_dataModel = new DataModel(this); m_view = new QTreeView(this); m_searchField = new QLineEdit(this); }
This approach makes developers familiar with Modern C++ paradigms slightly uncomfortable: a bunch of “raw news” in the code, no visible deletes — eww! Sure, you can replace the raw pointers with smart pointers, if you wish to. But that’s a discussion for another blog post.
- Another approach is to declare the owned objects as…objects, and not pointers:
class MyWidget : public QWidget { Q_OBJECT public: explicit MyWidget(QWidget *parent = nullptr); private: DataModel m_dataModel; // not pointers QTreeView m_view; QLineEdit m_searchField; }; MyWidget::MyWidget(QWidget *parent) : QWidget(parent) { }
This makes it completely clear that the lifetime of those objects is tied to the lifetime of MyWidget.
To Point, or Not to Point?
“So what is the difference between the two approaches?”, you may be wondering. That is just another way to ask: “which one is better and should I use in my code?”
The answers to this question involve a lot of interesting aspects, such as:
- Using pointers will significantly increase the number of memory allocations: we are going to do one memory allocation per child object. The irony here is that, most of the time, those child objects are, themselves, pimpl’d. Creating a QLineEdit object will, on its own, already allocate memory for its private data; we’re compounding that allocation with another one. Eventually, in m_searchField, we’ll store a pointer to a heap-allocated…”pointer” (the QLineEdit object, itself, which just contains the pimpl pointer) that points to another heap-allocate private object (QLineEditPrivate). Yikes!
- Pointers allow the user to forward declare the pointed-to datatypes in MyWidget‘s header. This means that users of MyWidget do not necessarily have to include the headers that define QTreeView, QlineEdit, etc. This improves compilation times.
- Pointers allow you to establish “grandchildren” and similar, not just direct children. A grandchild is going to be deleted by someone else (its parent), and not directly by our MyWidget instances. If that grandchild is a sub-object of MyWidget (like in the second design), this will mean destroying a sub-object via delete, and that’s bad.
- Pointers force (or, at least, should force) users to properly parent the allocated objects. This has an impact in a few cases, for instance if one moves the parent across threads. When using full objects as data members, it’s important to remember to establish a parent/child relationship by parenting them.
class MyObject : public QObject { Q_OBJECT QTimer m_timer; // timer as sub-object public: MyObject(QObject *parent) : QObject(parent) , m_timer(this) // remember to do this...! {} }; MyObject *obj = new MyObject; obj->moveToThread(anotherThread); // ...or this will likely break
Here the parent/child relationship is not going to be used to manage memory, but only to keep the objects together when moving them between threads (so that the entire subtree is moved by moveToThread).
So, generally speaking, using pointers seems to offer more advantages than disadvantages, and that is why they are so widely employed by developers that use Qt.
Const Correctness
All this is good and everything — and I’ve heard it countless times. What does all of this have to do with the title of the post? We’re getting there!
A consideration that I almost never hear as an answer to the pointer/sub-object debate is const correctness.
Consider this example:
class MyWidget : public QWidget { Q_OBJECT QLineEdit m_searchField; // sub-object public: explicit MyWidget(QWidget *parent = nullptr) : QWidget(parent) { } void update() const { m_searchField.setText("Search"); // ERROR } };
This does not compile; update() is a const method and, inside of it, m_searchField is a const QLineEdit. This means we cannot call setText() (a non-const method) on it.
From a design perspective, this makes perfect sense; in a const method we are not supposed to modify the “visible state” of *this and the “visible state” of QLineEdit logically belongs to the state of the *this object, as it’s a sub-object.
However, we have just discussed that the design of using sub-objects isn’t common; Qt developers usually employ pointers. Let’s rewrite the example:
class MyWidget : public QWidget { Q_OBJECT QLineEdit *m_searchField; // pointer public: explicit MyWidget(QWidget *parent = nullptr) : QWidget(parent) { m_searchField = new QLineEdit(this); } void update() const { m_searchField->setText("Search"); } };
What do you think happens here? Is the code still broken?
No, this code compiles just fine. We’re modifying the contents of m_searchField from within a const method.
Pointer-to-const and Const Pointers
This is not entirely surprising to seasoned C++ developers. In C++ pointers and references are shallow const; a const pointer can point to a non-const object and allow the user to mutate it.
Constness of the pointer and constness of the pointed-to object are two independent qualities:
// Non-const pointer, pointing to non-const object QLineEdit *p1 = ~~~; p1 = new QLineEdit; // OK, can mutate the pointer p1->mutate(); // OK, can mutate the pointed-to object // Const pointer, pointing to non-const object QLineEdit *const p2 = ~~~; p2 = new QLineEdit; // ERROR, cannot mutate the pointer p2->mutate(); // OK, can mutate the pointed-to // Non-const pointer, pointing to const object const QLineEdit *p3 = ~~~; p3 = new QLineEdit; // OK, can mutate the pointer p3->mutate(); // ERROR, cannot mutate the pointed-to object // Non-const pointer, just like p3, but using East Const (Qt uses West Const) QLineEdit const *p3b = ~~~; // Const pointer, pointing to const object const QLineEdit * const p4 = ~~~ ; p4 = new QLineEdit; // ERROR, cannot mutate the pointer p4->mutate(); // ERROR, cannot mutate the pointed-to object // Const pointer, just like p4, using East Const QLineEdit const * const p4b = ~~~;
This is precisely what is happening in our update() method. In there, *this is const, which means that m_searchField is a const pointer. (In code, this type would be expressed as QLineEdit * const.)
I’ve always felt mildly annoyed by this situation and have had my share of bugs due to modifications of subobjects from const methods. Sure, most of the time, it was entirely my fault, calling the wrong function on the subobject in the first place. But some of the time, this has made me have “accidental” const functions with visible side-effects (like the update() function above)!
Deep-const Propagation
The bad news is that there isn’t a solution for this issue inside the C++ language. The good news is that there is a solution in the “C++ Extensions for Library Fundamentals”, a set of (experimental) extensions to the C++ Standard Library. This solution is called std::experimental::propagate_const (cppreference, latest proposal at the time of this writing).
propagate_const acts as a pointer wrapper (wrapping both raw pointers and smart pointers) and will deeply propagate constness. If a propagate_const object is const itself, then the pointed-to object will be const.
// Non-const wrapper => non-const pointed-to object propagate_const<QLineEdit *> p1 = ~~~; p1->mutate(); // OK // Const wrapper => const pointed-to object const propagate_const<QLineEdit *> p2 = ~~~; p2->mutate(); // ERROR // Const reference to the wrapper => const pointed-to object const propagate_const<QLineEdit *> & const_reference = p1; const_reference->mutate(); // ERROR
This is great, because it means that we can use propagate_const as a data member instead of a raw pointer and ensure that we can’t accidentally mutate a child object:
class MyWidget : public QWidget { Q_OBJECT std::experimental::propagate_const<QLineEdit *> m_searchField; // drop-in replacement public: explicit MyWidget(QWidget *parent = nullptr) : QWidget(parent) { m_searchField = new QLineEdit(this); } void update() const { m_searchField->clear(); // ERROR! } };
Again, if you’re a seasoned C++ developer, this shouldn’t come as a surprise to you. My colleage Marc Mutz wrote about this a long time ago, although in the context of the pimpl pattern. KDAB’s RND efforts in this area led to things such as a deep-const alternative to QExplicitlySharedDataPointer (aptly named QExplicitlySharedDataPointerV2) and QIntrusiveSharedPointer.
propagate_const and Child QObjects
As far as I know, there hasn’t been much research about using propagate_const at large in a Qt-based project in order to hold child objects. Recently, I’ve had the chance to use it in a medium-sized codebase and I want to share my findings with you.
Compiler Support
Compiler support for propagate_const is still a problem, notably because MSVC does not implement the Library Fundamentals TS. It is, however, shipped by GCC and Clang and there are third party implementations available, including one in KDToolBox (keep reading!).
Source Compatibility
If one already has an existing codebase, one may want to start gradually adopting propagate_const by replacing existing usages of raw pointers. Unfortunately, in a lot of cases, propagate_const isn’t simply a drop-in replacement and will cause a number of source breaks.
What I’ve discovered (at the expense of my own sanity) is that some of these incompatibilities are caused by accidentally using niche C++ features; in order to be compatible with older C++ versions, implementations accidentally introduce quirks, some by compiler bugs.
Here’s all the nitty gritty details; feel free to skim over them 🙂
Broken Conversions to Superclasses
Consider this example:
class MyWidget : public QWidget { Q_OBJECT QLineEdit *m_searchField; public: explicit MyWidget(QWidget *parent = nullptr) : QWidget(parent) { // ... set up layouts, etc. ... m_searchField = new QLineEdit(this); layout()->addWidget(m_searchField); // this is addWidget(QWidget *) } };
Today, this works just fine. However, this does not compile:
class MyWidget : public QWidget { Q_OBJECT // change to propagate_const ... std::experimental::propagate_const<QLineEdit *> m_searchField; public: explicit MyWidget(QWidget *parent = nullptr) : QWidget(parent) { // ... set up layouts, etc. ... m_searchField = new QLineEdit(this); layout()->addWidget(m_searchField); // ^^^ ERROR, cannot convert propagate_const to QWidget * } };
C++ developers know the drill: smart pointer classes are normally not a 1:1 replacement of raw pointers. Most smart pointer classes do not implicitly convert to raw pointers, for very good reasons. In situations where raw pointers are expected (for instance, addWidget in the snippet above wants a parameter of type QWidget *), one has to be slightly more verbose, for instance, by calling m_searchField.get().
Here, I was very confused. propagate_const was meant to be a 1:1 replacement. Here are a couple of quotes from the C++ proposal for propagate_const:
The change required to introduce const-propagation to a class is simple and local enough to be enforced during code review and taught to C++ developers in the same way as smart-pointers are taught to ensure exception safety.
operator value*
When T is an object pointer type operator value* exists and allows implicit conversion to a pointer. This avoids using get to access the pointer in contexts where it was unnecesary before addition of the propagate_const wrapper.
In other words, it has always been a design choice to keep source compatibility in cases like the one above! propagate_const<T *> actually has a conversion operator to T *.
So why doesn’t the code above work? Here’s a reduced testcase:
std::experimental::propagate_const<Derived *> ptr; Derived *d1 = ptr; // Convert precisely to Derived *: OK Base *b1 = ptr; // Convert to a pointer to a base: ERROR Base *b2 = static_cast<Derived *>(ptr); // OK Base *b3 = static_cast<Base *>(ptr); // ERROR Base *b4 = ptr.get(); // OK
At first, this almost caused me to ditch the entire effort; Qt uses inheritance very aggressively and we pass pointers to derived classes to functions taking pointers to base classes all the time. If that required sprinkling calls to .get() (or casts) everywhere, it would have been a massive refactoring. This is something that a tool like clazy can automate for you; but that’s a topic for another time.
Still, I couldn’t find a justification for the behavior shown above. Take a look at this testcase where I implement a skeleton of propagate_const, focusing on the conversion operators:
template <typename T> class propagate_const { public: using element_type = std::remove_reference_t<decltype(*std::declval<T>())>; operator element_type *(); operator const element_type *() const; }; propagate_const<Derived *> ptr; Base *b = ptr; // OK
This now compiles just fine. Let’s make it 100% compatible with the specification by adding the necessary constraints:
template <typename T> class propagate_const { public: using element_type = std::remove_reference_t<decltype(*std::declval<T>())>; operator element_type *() requires (std::is_pointer_v<T> || std::is_convertible_v<T, element_type *>); operator const element_type *() const requires (std::is_pointer_v<T> || std::is_convertible_v<const T, const element_type *>); }; propagate_const<Derived *> ptr; Base *b = ptr; // still OK
This still works flawlessly. So what’s different with propagate_const as shipped by GCC or Clang?
libstdc++ and libc++ do not use constraints (as in, the C++20 feature) on the conversion operators because they want to make propagate_const also work in earlier C++ versions. In fact, they use the pre-C++20 way — we have to make overloads available only when certain conditions are satisfied: SFINAE.
The conversion operators in libstdc++ and libc++ are implemented like this:
template <typename T> class propagate_const { public: using element_type = std::remove_reference_t<decltype(*std::declval<T>())>; template <typename U = T, std::enable_if_t<(std::is_pointer_v<U> || std::is_convertible_v<U, element_type *>), bool> = true> operator element_type *(); template <typename U = T, std::enable_if_t<(std::is_pointer_v<U> || std::is_convertible_v<const U, const element_type *>), bool> = true> operator const element_type *() const; }; propagate_const<Derived *> ptr; Base *b = ptr; // ERROR
This specific implementation is broken on all major compilers, which refuse to use the operators for conversions other than to precisely element_type * and nothing else.
What’s going on? The point is that converting propagate_const to Base * is a user-defined conversion sequence: first we convert propagate_const to a Derived * through the conversion operator. Then, perform a pointer conversion from Derived * to Base *.
But here’s what The Standard says when templates are involved:
If the user-defined conversion is specified by a specialization of a conversion function template, the second standard conversion sequence shall have exact match rank.
That is, we cannot “adjust” the return type of a conversion function template, even if the conversion would be implicit.
The takeaway is: SFINAE on conversion operators is user-hostile.
Restoring the Conversions Towards Superclasses
We can implement a workaround here by deviating a bit from the specification. If we add conversions towards any pointer type that T implicitly converts to, then GCC and Clang (and MSVC) are happy:
template <typename T> class non_standard_propagate_const // not the Standard one! { public: using element_type = std::remove_reference_t<decltype(*std::declval<T>())>; // Convert to "any" pointer type template <typename U, std::enable_if_t<std::is_convertible_v<T, U *>, bool> = true> operator U *(); template <typename U, std::enable_if_t<std::is_convertible_v<const T, const U *>, bool> = true> operator const U *() const; }; non_standard_propagate_const<Derived *> ptr; Base *b = ptr; // OK
This is by far the simplest solution; it is, however, non-standard.
I’ve implemented a better solution in KDToolBox by isolating the conversion operators in base classes and applying SFINAE on the base classes instead.
For instance, here’s the base class that defines the non-const conversion operator:
// Non-const conversion template <typename T, bool = std::disjunction_v< std::is_pointer<T>, std::is_convertible<T, propagate_const_element_type<T> *> > > struct propagate_const_non_const_conversion_operator_base { }; template <typename T> struct propagate_const_non_const_conversion_operator_base<T, true> { constexpr operator propagate_const_element_type<T> *(); };
Then, propagate_const<T> will inherit from propagate_const_non_const_conversion_operator_base<T> and the non-template operator will be conditionally defined.
Deletion and Pointer Arithmetic
Quoting again from the original proposal for propagate_const:
Pointer arithemtic [sic] is not supported, this is consistent with existing practice for standard library smart pointers.
… or is it? Herb Sutter warned us about having implicit conversions to pointers, as they would also enable pointer arithmetic.
Consider again the C++20 version:
template <typename T> class propagate_const { public: using element_type = std::remove_reference_t<decltype(*std::declval<T>())>; operator element_type *() requires (std::is_pointer_v<T> || std::is_convertible_v<T, element_type *>); operator const element_type *() const requires (std::is_pointer_v<T> || std::is_convertible_v<const T, const element_type *>); }; propagate_const<SomeClass *> ptr; SomeClass *ptr2 = ptr + 1; // OK!
The arithmetic operators are not deleted for propagate_const. This means that the implicit conversion operators to raw pointers can (and will) be used, effectively enabling pointer arithmetic — something that the proposal said it did not want to support!
non_standard_propagate_const (as defined above), instead, does not support pointer arithmetic, as it needs to deduce the pointer type to convert to, and that deduction is not possible.
On a similar note, what should delete ptr; do, if ptr is a propagate_const object? Yes, there is some Qt-based code that simply deletes objects in an explicit way. (Luckily, it’s very rare.)
Again GCC and Clang reject this code, but MSVC accepts it:
template <typename T> class propagate_const { public: using element_type = std::remove_reference_t<decltype(*std::declval<T>())>; operator element_type *() requires (std::is_pointer_v<T> || std::is_convertible_v<T, element_type *>); operator const element_type *() const requires (std::is_pointer_v<T> || std::is_convertible_v<const T, const element_type *>); }; propagate_const<SomeClass *> ptr; delete ptr; // ERROR on GCC, Clang; OK on MSVC
It is not entirely clear to me which compiler is right, here. A delete expression requires us to convert its argument to a pointer type, using what the Standard defines as a contextual implicit conversion. Basically, the compiler needs to search for a conversion operator that can convert a propagate_const to a pointer and find only one of such conversion operators. Yes, there are two available but shouldn’t overload resolution select a best one? According to MSVC, yes; according to GCC, no.
(Note that this wording has been introduced by N3323, which allegedly GCC does not fully implement. I have opened a bug report against GCC.)
Anyways, remember what we’ve just learned: we are allowed to perform pointer arithmetic! That means that we can “fix” these (rare) usages by deploying a unary operator plus:
propagate_const<SomeClass *> ptr; delete +ptr; // OK! (ewww...)
That is pretty awful. I’ve decided instead to take my time and, in my project, refactor those “raw delete” by wrapping smart pointers instead:
propagate_const<std::unique_ptr<SomeClass>> ptr; // better
A Way Forward
In order to support propagate_const on all compilers, and work around the limitations of the upstream implementations explained above, I have reimplemented propagate_const in KDToolBox, KDAB’s collection of miscellaneous useful C++ classes and stuff. You can find it here.
I’ve, of course, also submitted bug reports against libstdc++ and libc++. It was promptly fixed in GCC (GCC 13 will ship with the fix). Always report bugs upstream!
The Results
- You can start using propagate_const and similar wrappers in your Qt projects, today. Some source incompatibilities are unfortunately present, but can be mitigated.
- An implementation of propagate_const is available in KDToolBox. You can use it while you wait for an upgraded toolchain with the bugs fixed. 🙂
- C++17 costs more. I cannot emphasize this enough. Not using the latest C++ standards costs more in development time, design, and debugging. While both GCC and MSVC (and upstream Clang) have very good C++20 support, Apple Clang is still lagging behind.
- SFINAE on conversion operators is user-hostile. The workarounds are even worse. Use concepts and constraints instead.
Thank you for reading!
If you like this article and want to read similar material, consider subscribing via our RSS feed.
Subscribe to KDAB TV for similar informative short video content.
KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.
Just RiiR🦀 already🙂
Today I learned Substitution failure is not an error.