0

This question is inspired by this question which was asking about calling the same method on dissimilar types when those types are known at compile time.

This got me thinking. Suppose I had dissimilar non-polymorphic types but I wanted use them polymorphically. Furthermore, I want to do that without ever invoking new and delete since these are known performance bottlenecks.

How would I do that?

Note this is a Q&A style question. I have provided the answer I came up with. This is not to attract upvotes (although that's always nice), but to share the insight I gained while working on this problem.

Other answers are certainly invited. The more knowledge we share, the better we all become.

Community
  • 1
  • 1
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142

1 Answers1

2

This answer was in-part inspired by the excellent work done by Beman Dawes on the boost::system_error library.

I learned the idea of static polymorphism by studying his fantastic work, which has now become part of the c++11 standard. Beman, if you ever read this please take a bow.

The other source of inspiration was the excellent talk called Inheritance is the base class of evil by the truly gifted Sean Parent. I thoroughly recommend every c++ developer to watch it.

So enough of that, here's (my) solution:

problem:

I have a number of UI object types that are not polymorphic (for performance reasons). However, sometimes I wish to call show() or hide() methods on groups of these objects.

Furthermore I want the references or pointers to these objects to be polymorphic.

Furthermore not all the objects even support the show() and hide() methods, but that shouldn't matter.

Furthermore the runtime performance overhead should be as close to zero as possible.

Many thanks to @Jarod42 for suggesting a less complicated constructor for showable.

my solutuion:

#include <iostream>
#include <vector>
#include <utility>
#include <typeinfo>
#include <type_traits>

// define an object that is able to call show() on another object, or emit a warning if that
// method does not exist
class call_show {

    // deduces the presence of the method on the target by declaring a function that either
    // returns a std::true_type or a std::false_type.
    // note: we never define the function. we just want to deduce the theoretical return type

    template<class T> static auto test(T* p) -> decltype(p->show(), std::true_type());
    template<class T> static auto test(...) -> decltype(std::false_type());

    // define a constant based on the above test using SFNAE
    template<class T>
    static constexpr bool has_method = decltype(test<T>(nullptr))::value;

public:

    // define a function IF the method exists on UIObject
    template<class UIObject>
    auto operator()(UIObject* p) const
    -> std::enable_if_t< has_method<UIObject>, void >
    {
        p->show();
    }

    // define a function IF NOT the method exists on UIObject
    // Note, we could put either runtime error handling (as below) or compile-time handling
    // by putting a static_assert(false) in the body of this function
    template<class UIObject>
    auto operator()(UIObject* p) const
    -> std::enable_if_t< not has_method<UIObject>, void >
    {
        std::cout << "warning: show is not defined for a " << typeid(UIObject).name() << std::endl;
    }
};

// ditto for the hide method
struct call_hide
{
    struct has_method_ {
        template<class T> static auto test(T* p) -> decltype(p->hide(), std::true_type());
        template<class T> static auto test(...) -> decltype(std::false_type());
    };

    template<class T>
    static constexpr bool has_method = decltype(has_method_::test<T>(nullptr))::value;

    template<class UIObject>
    auto operator()(UIObject* p) const
    -> std::enable_if_t< has_method<UIObject>, void >
    {
        p->hide();
    }

    template<class UIObject>
    auto operator()(UIObject* p) const
    -> std::enable_if_t< not has_method<UIObject>, void >
    {
        std::cout << "warning: hide is not defined for a " << typeid(UIObject).name() << std::endl;
    }
};

// define a class to hold non-owning REFERENCES to any object
// if the object has an accessible show() method then this reference's show() method will cause
// the object's show() method to be called. Otherwise, error handling will be invoked.
//
class showable
{
    // define the POLYMORPHIC CONCEPT of a thing being showable.
    // In this case, the concept requires that the thing has a show() and a hide() method
    // note that there is no virtual destructor. It's not necessary because we will only ever
    // create one model of this concept for each type, and it will be a static object
    struct concept {
        virtual void show(void*) const = 0;
        virtual void hide(void*) const = 0;
    };

    // define a MODEL of the CONCEPT for a given type of UIObject
    template<class UIObject>
    struct model final
    : concept
    {
        // user-provided constructor is necessary because of static construction (below)
        model() {};

        // implement the show method by indirection through a temporary call_show() object
        void show(void* p) const override {
            // the static_cast is provably safe
            call_show()(static_cast<UIObject*>(p));
        }

        // ditto for hide
        void hide(void* p) const override {
            call_hide()(static_cast<UIObject*>(p));
        }
    };

    // create a reference to a static MODEL of the CONCEPT for a given type of UIObject
    template<class UIObject>
    static const concept* make_model()
    {
        static const model<UIObject> _;
        return std::addressof(_);
    }

    // this reference needs to store 2 pointers:

    // first a pointer to the referent object
    void * _object_reference;

    // and secondly a pointer to the MODEL appropriate for this kind of object
    const concept* _call_concept;

    // we use pointers because they allow objects of the showable class to be trivially copyable
    // much like std::reference_wrapper<>

public:

    // PUBLIC INTERFACE

    // special handling for const references because internally we're storing a void* and therefore
    // have to cast away constness
    template<class UIObject>
    showable(const UIObject& object)
    : _object_reference(const_cast<void*>(reinterpret_cast<const void *>(std::addressof(object))))
    , _call_concept(make_model<UIObject>())
    {}

    template<class UIObject>
    showable(UIObject& object)
    : _object_reference(reinterpret_cast<void *>(std::addressof(object)))
    , _call_concept(make_model<UIObject>())
    {}

    // provide a show() method.
    // note: it's const because we want to be able to call through a const reference
    void show() const {
        _call_concept->show(_object_reference);
    }

    // provide a hide() method.
    // note: it's const because we want to be able to call through a const reference
    void hide() const {
        _call_concept->hide(_object_reference);
    }
};


//
// TEST CODE
//

// a function to either call show() or hide() on a vector of `showable`s
void show_or_hide(const std::vector<showable>& showables, bool show)
{
    for (auto& s : showables)
    {
        if (show) {
            s.show();
        }
        else {
            s.hide();
        }
    }
}

// a function to transform any group of object references into a vector of `showable` concepts
template<class...Objects>
auto make_showable_vector(Objects&&...objects)
{
    return std::vector<showable> {
        showable(objects)...
    };
}


int main()
{
    // declare some types that may or may not support show() and hide()
    // and create some models of those types

    struct Window{
        void show() {
            std::cout << __func__ << " Window\n";
        }
        void hide() {
            std::cout << __func__ << " Window\n";
        }

    } w1, w2, w3;

    struct Widget{

        // note that Widget does not implement show()

        void hide() {
            std::cout << __func__ << " Widget\n";
        }

    } w4, w5, w6;

    struct Toolbar{
        void show()
        {
            std::cout << __func__ << " Toolbar\n";
        }

        // note that Toolbar does not implement hide()

    } t1, t2, t3;

    struct Nothing {
        // Nothing objects don't implement any of the functions in which we're interested
    } n1, n2, n3;

    // create some polymorphic references to some of the models
    auto v1 = make_showable_vector(w3, w4, n1, w5, t1);
    auto v2 = make_showable_vector(n3, w1, w2, t2, w6);

    // perform some polymorphic actions on the non-polymorphic types
    std::cout << "showing my UI objects\n";
    show_or_hide(v1, true);
    show_or_hide(v2, true);

    std::cout << "\nhiding my UI objects\n";
    show_or_hide(v2, false);
    show_or_hide(v1, false);

    return 0;
}

example output:

showing my UI objects
show Window
warning: show is not defined for a Z4mainE6Widget
warning: show is not defined for a Z4mainE7Nothing
warning: show is not defined for a Z4mainE6Widget
show Toolbar
warning: show is not defined for a Z4mainE7Nothing
show Window
show Window
show Toolbar
warning: show is not defined for a Z4mainE6Widget

hiding my UI objects
warning: hide is not defined for a Z4mainE7Nothing
hide Window
hide Window
warning: hide is not defined for a Z4mainE7Toolbar
hide Widget
hide Window
hide Widget
warning: hide is not defined for a Z4mainE7Nothing
hide Widget
warning: hide is not defined for a Z4mainE7Toolbar
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • Any reason to have `vector` instead of a `tuple` and iterate over the tuple ? – Jarod42 Dec 18 '15 at 18:37
  • BTW, why not use the 2 overloads `showable(const UIObject& object)`/`showable(UIObject& object)` instead of SFINAE on `is_const`. – Jarod42 Dec 18 '15 at 18:38
  • @Jarod42 yes. it's to demonstrate that the `showable` reference is polymorphic, so functions can be written in terms of the `showable` concept, for example you can now pass an arbitrary run-time collection of showables to a function. Those showables will be referring to type-erased non-poly objects. – Richard Hodges Dec 18 '15 at 18:38
  • Your `void*` is unneeded, you may put member directly inside model (you have to remove the static singleton). If the `static` was to avoid allocation, you may still do a placement new. – Jarod42 Dec 19 '15 at 01:47
  • @Jarod42 we did that how would we point to 2 different referents of the same type? We'd need a memory allocation, surely? – Richard Hodges Dec 19 '15 at 07:09
  • In fact, you have almost reimplemented virtual function machinery inside your smart reference class. In your case the `concept` objects serve as virtual function tables, they are global, allocated on "per type" basis. And you have to store VFT pointer in a reference, because you are not storing it in the object itself. The code is quite large to write it for every set of methods you want to make polymorphic in a program =) But anyway, the idea of polymorphic references is cool. I wonder if it were discussed in the area of C++ standard or boost. – stgatilov Dec 21 '15 at 14:25
  • @stgatilov the code is large but quite boilerplate-like. I'm pretty sure an afternoon's work could boil it down to a class template that we simply specialise with a traits class for each method we want to support. – Richard Hodges Dec 21 '15 at 15:13