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