Sign up for the KDAB Newsletter
Stay on top of the latest news, publications, events and more.
Go to Sign-up
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.
The answer becomes slightly more nuanced when MyWidget
actually owns the objects in question. In this case, the implementation designs are basically two:
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 new
s" in the code, no visible delete
s -- eww! Sure, you can replace the raw pointers with smart pointers, if you wish to. But that's a discussion for another blog post.
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
.
"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:
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!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.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.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.
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.
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)!
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
.
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 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!).
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 :)
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.
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.
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
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!
propagate_const
and similar wrappers in your Qt projects, today. Some source incompatibilities are unfortunately present, but can be mitigated.propagate_const
is available in KDToolBox. You can use it while you wait for an upgraded toolchain with the bugs fixed. :-)Thank you for reading!
About KDAB
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
Stay on top of the latest news, publications, events and more.
Go to Sign-up
Upgrade your applications from Qt 5 to Qt 6 with KDAB’s migration services. Get a free migration assessment and join a hands-on workshop to prepare your team for a successful transition!
Learn more
Learn Modern C++
Our hands-on Modern C++ training courses are designed to quickly familiarize newcomers with the language. They also update professional C++ developers on the latest changes in the language and standard library introduced in recent C++ editions.
Learn more
1 Comment
13 - Feb - 2023
skierpage
Just RiiR🦀 already🙂 Today I learned Substitution failure is not an error.