Polymorphism and Implicit Sharing
Recently I have been researching into possibilities to make members of KoShape
copy-on-write.
At first glance, it seems enough to declare d-pointers as some subclass of QSharedDataPointer
(see Qt's implicit sharing) and then replace
pointers with instances. However, there remain a number of problems to be solved, one of them
being polymorphism.
polymorphism and value semantics
In the definition
of KoShapePrivate
class, the member fill
is stored as a QSharedPointer
:
QSharedPointer<KoShapeBackground> fill;
There are a number of subclasses of KoShapeBackground
, including KoColorBackground
,
KoGradientBackground
, to name just a few. We cannot store an instance of KoShapeBackground
directly since we want polymorphism. But, well, making KoShapeBackground
copy-on-write seems to have
nothing to do with whether we store it as a pointer or instance. So let's just put it here --
I will come back to this question at the end of this post.
d-pointers and QSharedData
The KoShapeBackground
heirarchy (similar to the KoShape
one) uses derived d-pointers
for storing private data. To make things easier, I will here use a small example to
elaborate on its use.
derived d-pointer
1 | class AbstractPrivate |
The main goal of making DerivedPrivate
a subclass of AbstractPrivate
is to avoid multiple
d-pointers in the structure. Note that there are constructors taking a reference to the
private data object. These are to make it possible for a Derived
object to use the same
d-pointer as its Abstract
parent. The Q_D()
macro is used to convert the d_ptr
, which is a
pointer to AbstractPrivate
to another pointer, named d
, of some of its descendent type;
here, it is a DerivedPrivate
. It is used together with the Q_DECLARE_PRIVATE()
macro
in the class definition
and has a rather complicated implementation in the Qt headers. But for simplicity, it does
not hurt for now to understand it as the following:
#define Q_D(Class) Class##Private *const d = reinterpret_cast<Class##Private *>(d_ptr.data())
where Class##Private
means simply to append string Private
to (the macro argument) Class
.
Now let's test it by creating a pointer to Abstract
and give it a Derived
object:
1 | int main() |
Output:
foo 0 0
foo 1 1
Looks pretty viable -- everything's working well! -- What if we use Qt's implicit sharing? Just
make AbstractPrivate
a subclass of QSharedData
and replace QScopedPointer
with QSharedDataPointer
.
making d-pointer QSharedDataPointer
In the last section, we commented out the copy constructors since QScopedPointer
is not copy-constructable,
but here QSharedDataPointer
is copy-constructable, so we add them back:
1 | class AbstractPrivate : public QSharedData |
And testing the copy-on-write mechanism:
1 | int main() |
But, eh, it's a compile-time error.
error: reinterpret_cast from type 'const AbstractPrivate*' to type 'AbstractPrivate*' casts away qualifiers
Q_DECLARE_PRIVATE(Abstract)
Q_D
, revisited
So, where does the const
removal come from? In qglobal.h
, the code related to Q_D
is as follows:
1 | template <typename T> inline T *qGetPtrHelper(T *ptr) { return ptr; } |
It turns out that Q_D
will call d_func()
which then calls an overload of qGetPtrHelper()
that takes const Ptr &ptr
. What does ptr.operator->()
return? What is the difference between
QScopedPointer
and QSharedDataPointer
here?
QScopedPointer
's operator->()
is a
const
method that returns a non-const
pointer to T
; however,
QSharedDataPointer
has two
operator->()
s, one being const T* operator->() const
, the other T* operator->()
, and they
have quite different behaviours -- the non-const
variant calls detach()
(where copy-on-write
is implemented), but the other one does not.
qGetPtrHelper()
here can only take d_ptr
as a const QSharedDataPointer
, not a non-const
one; so, no matter which d_func()
we are calling, we can only get a const AbstractPrivate *
.
That is just the problem here.
To resolve this problem, let's replace the Q_D
macros with the ones we define ourselves:
#define CONST_SHARED_D(Class) const Class##Private *const d = reinterpret_cast<const Class##Private *>(d_ptr.constData())
#define SHARED_D(Class) Class##Private *const d = reinterpret_cast<Class##Private *>(d_ptr.data())
We will then use SHARED_D(Class)
in place of Q_D(Class)
and CONST_SHARED_D(Class)
for
Q_D(const Class)
. Since the const
and non-const
variant really behaves differently,
it should help to differentiate these two uses. Also, delete Q_DECLARE_PRIVATE
since we
do not need them any more:
1 | class AbstractPrivate : public QSharedData |
With the same main()
code, what's the result?
foo 0 0
foo 1 16606417
foo 0 0
... big whoops, what is that random thing there? Well, if we use dynamic_cast
in place of
reinterpret_cast
, the program simply crashes after ins->modifyVar();
, indicating that
ins
's d_ptr.data()
is not at all a DerivedPrivate
.
virtual clones
The detach()
method of QSharedDataPointer
will by default create an instance of AbstractPrivate
regardless of what the instance really is. Fortunately, it is possible to change that behaviour
through specifying the clone()
method.
First, we need to make a virtual function in AbstractPrivate
class:
virtual AbstractPrivate *clone() const = 0;
(make it pure virtual just to force all subclasses to re-implement it; if your base class is
not abstract you probably want to implement the clone()
method) and then override it
in DerivedPrivate
:
virtual DerivedPrivate *clone() const { return new DerivedPrivate(*this); }
Then, specify the template method for QSharedDataPointer::clone()
. As we will re-use it multiple
times (for different base classes), it is better to define a macro:
1 |
|
It is not necessary to write DATA_CLONE_VIRTUAL(Derived)
as we are never storing a
QSharedDataPointer<DerivedPrivate>
throughout the heirarchy.
Then test the code again:
foo 0 0
foo 1 1
foo 0 0
-- Just as expected! It continues to work if we replace Derived
with Abstract
in QScopedPointer
:
QScopedPointer<Abstract> ins(new Derived());
QScopedPointer<Abstract> ins2(new Derived(* dynamic_cast<const Derived *>(ins.data())));
Well, another problem comes, that the constructor for ins2
seems too ugly, and messy. We could, like
the private classes, implement a virtual function clone()
for these kinds of things, but it is
still not gentle enough, and we cannot use a default copy constructor for any class that contains
such QScopedPointer
s.
What about QSharedPointer
that is copy-constructable? Well, then these copies actually point to
the same data structures and no copy-on-write is performed at all. This still not wanted.
the Descendent
s of ...
Inspired by Sean Parent's video, I finally come up with the following implementation:
1 | template<typename T> |
This class allows you to use Descendent<T>
(read as "descendent of T
") to represent any instance
of any subclass of T
. It is copy-constructable, move-constructable, copy-assignable, and move-assignable.
Test code:
1 | int main() |
It gives just the same results as before, but much neater and nicer -- How does it work?
First we define a class concept
. We put here what we want our instance to satisfy. We would like to
access it as const
and non-const
, and to clone it as-is. Then we define a template class model<U>
where U
is a subclass of T
, and implement these functionalities.
Next, we store a unique_ptr<concept>
. The reason for not using QScopedPointer
is QScopedPointer
is not
movable, but movability is a feature we actually will want (in sink arguments and return values).
Finally it's just the constructor, moving and copying operations, and ways to access the wrapped object.
When Descendent<Abstract> ins2 = ins;
is called, we will go through the copy constructor of Descendent
:
Descendent(const Descendent & that) : m_d(move(that.m_d->clone())) {}
which will then call ins.m_d->clone()
. But remember that ins.m_d
actually contains a pointer to
model<Derived>
, whose clone()
is return make_unique<model<Derived> >(Derived(instance));
. This expression
will call the copy constructor of Derived
, then make a unique_ptr<model<Derived> >
, which calls the
constructor of model<Derived>
:
model(Derived x) : instance(move(x)) {}
which move-constructs instance
. Finally the unique_ptr<model<Derived> >
is implicitly converted to
unique_ptr<concept>
, as per the conversion rule.
"If T
is a derived class of some base B
, then std::unique_ptr<T>
is implicitly convertible to
std::unique_ptr<B>
."
And from now on, happy hacking --- (.>w<.)