User Tools

Site Tools


templated_covariant_pointer_type_in_c_with_simple_inheritance

Inheritance, Covariance & More (Template Metaprogramming)

  • Problem. I can't implement a covariant “pointer” type to which I can add my own features (notably automatic nulling) but also access a ref counting etc.
  • My solution to this is more about demonstrating the walking and iteration of data structures than any practical use
  • Several C++ features are missing from this, notably multiple inheritance. Some curse at multiple inheritance but as C++ has only the single feature for object model programming (unlike other languages with built in “category”, “interface”, “extends” features I use multiple inheritance a lot, especially for interfaces.

Please find all the code for this on github at https://github.com/kuiash/covariant_pointer_templates.

Inheritance

Only single inheritance is shown in the example. Multiple inheritance is possible with the addition of type lists. More to come on that as it shows further interesting ways of walking data structures.

To start with inheritance. A simple template is used to decorate each class with an _t_inherits type member that “points” to the parent class. It also performs the actual inheritance for us. Note that I don't support protection. Why? Because I'm a heretic and also it's out of scope of this exercise!

template<typename _parent>
    struct inherits : _parent
    {
        using _t_inherits = _parent;
    };

Object Type

So now I will define a basic object type that represents the top of the tree. All classes in this hierarchy will ultimately derive from this.

Notice that it contains nothing. That is fine as it is used only for type comparison at compile time.

struct object
{ };

That's all I need. It just marks the top of the inheritance tree.

Test Hierarchy

Next I will define a simple class hierarchy that produces a tree at compile time that can be walked (upwards) by the isa code.

////////////////////////////////////////////////////////////////
///
///            test hierarchy
///
///         .---------object----.
///         va                  vb
///         |                   |
///         vaa                 vbb-----.
///                             |       |
///                             vbbb    vbbc
///
////////////////////////////////////////////////////////////////
 
struct va : inherits<object>
{ };
 
struct vb : inherits<object>
{ };
 
struct vbb : inherits<vb>
{ };
 
struct vaa : inherits<va>
{ };
 
struct vbbb : inherits<vbb>
{ };
 
struct vbbc : inherits<vbb>
{ };

IS A, ISA, IS_A

Now we know this we can implement a template meta function that can tell us if a class of object of a class (use decltype or let the template processor do the work for you).

template<typename _class, typename _isa> struct isa;
 
template<>
    struct isa<object, object>
    {
        using _t_isa = t_true;
    };
 
template<typename _class>
    struct isa<_class, _class>
    {
        using _t_isa = t_true;
    };
 
template<typename _class>
    struct isa<object, _class>
    {
        using _t_isa = t_false;
    };
 
template<typename _class, typename _isa>
    struct isa
    {
        using _t_isa = typename isa<typename _class::_t_inherits, _isa>::_t_isa;
    };

Let's not be scared yet!

The first declaration simply defines what the template looks like. After this I use varying degrees of specialisation to provide the correct answers.

The next template is the isa definition for object == object. Because, well, it is. Any object pointer is also an object pointer.

The next template is invoked when both the _class and the _isa match. The equivalent of variable equality (that is the variables have different names but the same value). This represents a successful match. Clearly a pointer to my_object is also a pointer to my_object.

The next template <object, _class> this means you have walked all the way to the top of the tree and not managed to find a match for _isa. In this case _t_isa it set the the false type.

The final template is the one that does the actual work.

It recursively walks up the tree using the _t_inherits type member of each class. When one of the above matches is found the _t_isa member is set to t_false or t_true.

t_true and t_false are simple type wrappers around a true or false bool value. This can be decoded at runtime and compile time and used, for example, with static_assert.

Covariant Pointer Templates

So now we can determine if one class/object is derived from another quite easily. We've seen how we can walk a tree (upwards) and return true/false values. One can even return multiple values from templates as they are named, recursion is possible and (as I will show in a later article) so are lists, stacks and more.

template<typename _inner>
    struct ptr
    {
        using _t_inner = _inner;
        _inner * _data;
 
        ptr() : _data(nullptr)
        { }
 
        ptr(_inner * _data) : _data(_data)
        { }
 
        template<typename _t> ptr(const ptr<_t> _that)
        {
            // note; rather annoyingly you can't put anything in
            // static_assert other than a string literal which
            // really limits what one can do (static const char * FTW??)
            static_assert(
                isa<_t, _inner>::_t_isa::_v,
                "Cannot cast pointer. Incompatible types");
 
            _data = _that._data;
        }
    };

What's going on here is that we have a pointer template. It is not a particularly complete definition but that's because I want to demonstrate the compile time failure due to incorrect inheritance.

The important part of this is the templated constructor.

The type _t in that definition is the incoming type. That is the type that is assigned from (source). The ptr being written to (this) has type _t_inner (outside the class) and _inner inside the class.

The isa template is used to decide if the pointer can be upcast.

If it can be then we just go ahead and let the compiler do it's job.

If it can't then static_assert causes the compilation to finish and emit and error.

Referring back the the test hierarchy we can see that, for example, all classes derive from object, vbbc derives from vb via *vbb*.

Running the test app (see github link up above) shows this output. The actual test application is quite exhaustive.

"(isa<object,object>::_t_isa)" == t_true
"(isa<va,object>::_t_isa)" == t_true
"(isa<vaa,object>::_t_isa)" == t_true
"(isa<vb,object>::_t_isa)" == t_true
"(isa<vbb,object>::_t_isa)" == t_true
"(isa<vbbb,object>::_t_isa)" == t_true
"(isa<vbbc,object>::_t_isa)" == t_true
"(isa<object,vbb>::_t_isa)" == t_false
"(isa<va,vbb>::_t_isa)" == t_false
"(isa<vaa,vbb>::_t_isa)" == t_false
"(isa<vb,vbb>::_t_isa)" == t_false
"(isa<vbb,vbb>::_t_isa)" == t_true
"(isa<vbbb,vbb>::_t_isa)" == t_true
"(isa<vbbc,vbb>::_t_isa)" == t_true

Notes

  • Unordered List ItemError reporting is not fantastic but hopefully C++ concepts will help here, however, you'll have to wait until 2020 at least for really good compiler support in your own toolchains.
  • Down casting is not supported at all, this is a very different problem that requires run time understanding of objects. Often via a “vtable”. I'm seriously considering how those can be generated via templates!
  • static_assert has very limited
templated_covariant_pointer_type_in_c_with_simple_inheritance.txt · Last modified: 2017/09/05 09:18 by xylene