Design and use of value classes
The canonical value class
Our value classes all follow a similar pattern of common methods:
- A
registerMetadata
method to register the class with the metatype system. - Stringification methods to generate a string representation of an object.
- Comparison operators for comparing two objects of the same class.
- An overload of
qHash
to generate a hash of an object. - Methods to marshal and unmarshal an object as a DBus argument.
- Methods to serialize to and from a JSON structure.
- A method to get the icon representing the object.
- Methods to access the properties of the object by an index.
The most common case, in which the methods are implemented using the
metaclass
{.interpreted-text role=“ref”} system, is supported by
deriving the class from CValueObject
, which is a convenience class
template which derives from all the most commonly used mixin classes,
using multiple inheritance.
Meta types and CVariant
Any of our value objects can be stored in a [CVariant]{.title-ref}, and
the canonical methods of the stored object can be accessed through the
methods of CVariant
. This is facilitated by a meta type system built
on top of Qt's meta type system, using
BlackMisc::registerMetaValueType()
and duck
typing. This allows value
objects to be treated polymorphically while retaining value semantics
and without introducing virtual methods in the public API.1 It is not
neccessary for all of the canonical methods to be present in an object
stored in a CVariant
; it is a runtime error to attempt to use a method
of a CVariant
storing an object which doesn't support that method.
Mixin classes
::: {.note} ::: {.title} Note :::
Some Q&A about mixins from the meetings here. :::
Sometimes it doesn't make sense for a particular value class to
implement one or more of the canonical methods. In these cases, instead
of deriving from CValueObject
, a value class can derive directly from
the specific mixin classes which it needs. This allows for extensive
code reuse through inheritance without violating the Liskov
substitution
principle
and without coupling together unrelated responsibilities. All of the
mixin classes, as well as CValueObject
, use the
CRTP.
Some of the mixins require their template argument type to satisfy
particular
concepts.
This is documented in their Doxygen class documentation. The concepts
required when deriving from CValueObject
are the union of all the
required concepts of the mixins which CValueObject
uses.
Inheritance
Occasionally, we have value classes which represent specializations of
other value classes, and therefore derive from those other value
classes. For example, CHeading
derives from CAngle
, and CAltitude
derives from CLength
. In these cases, there is a base class which
derives from certain mixins, and a derived class which derives from the
base class and some other mixins. A base and a derived class might
derive from some of the same mixins. For example, CAngle
derives from
Mixin::MetaType<CAngle>
and CHeading
derives from
Mixin::MetaType<CHeading>
. In these cases, it is necessary to use a
using
declaration
to disambiguate which methods should be inherited in the derived class.
In this example, the definition of CHeading
has a
using Mixin::MetaType<CHeading>::registerMetadata;
to tell the
compiler that it wants to inherit the registerMetadata
method of
Mixin::MetaType<CHeading>
, not the registerMetadata
method of
CAngle
. To reduce duplication of these using
declarations, there are
macros provided with the mixin classes to automatically introduce the
appropriate declarations into a derived class.
Virtual methods in frequently-used CRTP class templates have a detrimental effect on compilation times, because they inhibit lazy instantiation of methods. ↩︎