Decided to roll out custom RTTI.

Started by
6 comments, last by Codarki 15 years, 1 month ago
So I'll ask my question before the gory details. Is it guaranteed that there is unique static const char* within a program, for the following? static char const my_constant[] = "constant_text"; I was to implementet event handling framework for my C++ UI lib. My goals was: - Simple to use and relatively simple to extend. - Separate handling of different events from each others, ie input handler does not need to know about paint events. - To allow components implement only specific event handlers. - Avoid dynamic_cast. - Extensible events and handlers. UI paint event does not know about graphics API, therefore I create matching d3d_paint_event, which has graphics device in its context, and handler for it. Now my first approach was to have abstract event handler interface for each event type. Then have event_dispatch_table inherit these handlers for double dispatch, then act as proxy and redirect them to invidual handlers. This worked ok, but it soon became apparent I would have to write bunch of proxy classes for stripping down the unwanted handlers, and bunch of adapter classes for converting events to graphics API or project specific events. Too much boiler plate. To avoid dynamic casts, next approach was kind of std::map of std::type_info to event handlers, to query handlers runtime. It seems std::type_info operator== is not much better than dynamic_cast, it performs strcmp() under the hood. No good. My requirements for std::type_info aren't so strict. I dont need to support multiple inheritance, nor any issues with DLLs (vtlb/type_info replication). Tried to stay away from implementing my own RTTI, but my options ran out. So I came up with this, using char* comparison. Is it guaranteed that there is unique static const char* within a program? Simplified example, compiles and works. I templatised the id, so it could be specialized to std::type_info aswell.

#include <boost/shared_ptr.hpp>
#include <map>

// helper struct to allow partial specialization for Event.
template<typename Event, typename Id = char const*>
struct handler_id_for_event;

// Event A
struct event_a_handler_base;
struct event_a
{
    typedef event_a_handler_base handler_type;
};

// Event A handler
struct event_a_handler_base
{
    typedef event_a event_type;
    virtual ~event_a_handler_base() {}
    virtual void handle(event_a& e) = 0;
};

template<typename Id = char const*>
struct event_a_handler : public event_a_handler_base
{
    static Id id();
};

template<>
char const* event_a_handler<char const*>::id()
{
    return "event_a_handler";
}

template<typename Id>
struct handler_id_for_event<event_a,Id>
{
    // Implicit conversion to Id.
    operator Id()
    {
        return event_a_handler<Id>::id();
    }
};

// Event B
struct event_b_handler_base;
struct event_b
{
    typedef event_b_handler_base handler_type;
};

// Event B handler
struct event_b_handler_base
{
    typedef event_b event_type;
    virtual ~event_b_handler_base() {}
    virtual void handle(event_b& e) = 0;
};

template<typename Id = char const*>
struct event_b_handler : public event_b_handler_base
{
    static Id id();
};

template<typename Id>
Id event_b_handler<Id>::id()
{
    return "event_b_handler";
}

template<typename Id>
struct handler_id_for_event<event_b,Id>
{
    // Implicit conversion to Id.
    operator Id()
    {
        return event_b_handler<Id>::id();
    }
};

// Dispatcher.
template<typename Id = char const*>
class event_dispatch_table
{
public:
    // Handler is required to implement function Id Handler::id().
    template<typename Handler>
    void insert(boost::shared_ptr<Handler> handler)
    {
        assert(m_handlers.find(Handler::id()) == m_handlers.end());
        m_handlers[Handler::id()] = handler;
    }

    // Event is required to provide Event::handler_type().
    // Also specialization for struct handler_id_for_event<Event> is
    // required.
    template<typename Event>
    void dispatch(Event& e)
    {
        Event::handler_type* handler
            = static_cast<Event::handler_type*>(handler_for_event<Event>());
        handler->handle(e);
    }

private:
    template<typename Event>
    void* handler_for_event()
    {
        Id id = handler_id_for_event<Event>();
        assert(m_handlers.find(id) != m_handlers.end());
        assert(m_handlers[id]);
        return m_handlers[id].get();
    }

private:
    std::map<Id,boost::shared_ptr<void> > m_handlers;
};

namespace {
    struct simple_a_handler : public event_a_handler<>
    {
        void handle(event_a&) {}
    };
    struct simple_b_handler : public event_b_handler<>
    {
        void handle(event_b&) {}
    };
}

int main()
{
    event_dispatch_table<> table;
    table.insert(
        boost::shared_ptr<event_a_handler<> >(new simple_a_handler));
    table.insert(
        boost::shared_ptr<event_b_handler<> >(new simple_b_handler));

    event_a a;
    event_b b;
    table.dispatch(a);
    table.dispatch(b);

    return 0;
}

Advertisement
I just answer one of your questions (lack of time, sorry [smile]).

static char const my_constant[] = "constant_text";


The "static" says it is only visible inside the current translation unit (which btw is, afair, deprecated in favor of anonymous namespaces). To make it have external linkage (and hence the same pointer address over all units of translation), do either

char const my_constant[] = "constant_text";

or better (say what you mean :D)
extern char const my_constant[] = "constant_text";

.

edit: This is assuming those declarations are at file scope, not function scope. It you put those in function scope, then they can't be extern.

[Edited by - phresnel on March 4, 2009 2:44:29 AM]
Quote:It seems std::type_info operator== is not much better than dynamic_cast, it performs strcmp() under the hood. No good.

My implementation doesn't.

Quote:Is it guaranteed that there is unique static const char* within a program?

There will be one in each translation unit you define one.
If dynamic_cast is your problem, you should try to design an event system that does not _need_ dynamic_cast, instead of reimplementing dynamic_cast.
Quote:Original post by Ftn
So I'll ask my question before the gory details. Is it guaranteed that there is unique static const char* within a program, for the following?
static char const my_constant[] = "constant_text";

I don't think so. I believe that the common optimization of finding identical static string data and reducing can easily be applied, and result in shared storage in that case.

static just means the symbol won't be shared, it says nothing about the data. And if you change static const global data, the resulting behaviour is (I think?) undefined by the standard. (Or was it unspecified?)

Note that while you can fold together identical strings, there is no guarantee that it will be done. So two different compilation units could get different, or the same, value for &my_constant[0].
Quote:Tried to stay away from implementing my own RTTI, but my options ran out. So I came up with this, using char* comparison. Is it guaranteed that there is unique static const char* within a program? Simplified example, compiles and works. I templatised the id, so it could be specialized to std::type_info aswell.

A nice portable trick I use is I build a string registry.

It takes a char*, and returns a struct RegisteredString { const char* debug_data; }.

If two char* lexicographically compare equal, then the RegisteredString generated is guaranteed to compare equal.

Useful methods include "RegisteredString getRegisteredString( const char* )" and "RegisteredString isRegistered( const char* )".

A NULL RegisteredString is an invalid RegisteredString.

Unless you are a friend of RegisteredString, you CANNOT generate a RegisteredString from a const char*, or any other way other than copy-construct, trivial-construct, or assignment from a RegisteredString. The only friend of RegisteredString is the registry class.

You get readable-at-debugging time objects that sort based on operator< in O(1) time regardless of the length of the string data. If a class needs access to a unique registered string, it isn't that hard to only call the getRegisteredString() method once.
phresnel:
Thanks. I belive placing this static char const* inside member function in to a .cpp, and accessing it by static member function in .h also gives external linkage.

Quote:Original post by loufoque
Quote:It seems std::type_info operator== is not much better than dynamic_cast, it performs strcmp() under the hood. No good.

My implementation doesn't.

Went through disassembly, mine does it with strcmp(). (MSVC)

Quote:Original post by Rattenhirn
If dynamic_cast is your problem, you should try to design an event system that does not _need_ dynamic_cast, instead of reimplementing dynamic_cast.

I have double dispatch working without any casts, I didnt refactor this code into my codebase yet. My problem was my requirement to add project specific information to UI events, by making new project specific event classes which composite the UI event they are extending. This requires a new handler aswell.

Since visitor in visitor pattern (how my double dispatch is implemented) needs to know about all the events and handlers, and I dont want my UI to know about project specific events, I would have to create visitor in project which extends the events without UI knowing about it. That and dropping handlers which components dont need to know about, is the cause of alot of visitor permutations.

I found my requirement to extend events unreasonable. The abstraction I was trying to achieve does not work, since every component handling extended events already has dependency to this extended information by knowning about them.

Therefore, code presented here is dropped, and I provide these informations (ie current backbuffer) with another interfaces.

NotAYakk:
Yea I have a similar version of string registry aswell.
Hello, I'm going to share how I implemented event handling in C++ (such as GUI events but not limited to) in my general purpose library which I use as a base of my ongoing pet engine project. Hope it can serve for inspiration...

I modeled my C++ event handling system after the .Net framework and the observer pattern. Properties of the system can be summed up:

- For one event there can be any number of event handlers (I call them 'delegates')
- When the event is raised, all of its handlers are invoked
- There's no guarantee about the order in which handlers are invoked
- So actions of event handlers for the same event should be independent. When that is not possible, use only one handler for the event.

That were pretty much the general observer pattern properties, now how my implementation works:

- There are two main classes: Event and Delegate.
- Delegate essentially wraps a function pointer into a nice Delegate object.
- Event is just a list of Delegate objects with a method that calls all the assigned delegates and with some convenience overloaded operators.
- A class that exposes events declares a public Event<T> member (T is type of the event args, i.e. data type that is passed to event handlers when the event is raised.
- Adding an handler is as easy as: o.Clicked += new Delegate<T>(&some_function);
- Raising the event is as easy as calling: o.Clicked.Raise(args); where 'args' is a variable of type T.
- Pretty much anything can be used as T and passed to event handlers. But the handlers must know about T (that means a handler that uses ClickEventArgs struct for T cannot do the job for event that requires KeyEventArgs and vice versa).
- Also only delegates with the same T as the event's T can be assigned to the event.
- And it works completely without RTTI.

Now better than thousand words is the code:

// This is a helper class to abstract the concept of Delegate as a function pointer.// T = type of arg passed to the delegate methodtemplate <class T> class DelegateBase : public Object{public:    virtual void Invoke(void * p_sender, T p_event_args) = 0;};            // This class represents a concrete function pointer which can be an instance method or static method.// CLASS = type whose member method we are going to wrap in this Delegate object.// T = type of arg passed to the delegate methodtemplate <class CLASS, class T> class Delegate : public DelegateBase<T>{public:    typedef void (CLASS::*Method)(void*, T);    typedef void (*StaticMethod)(void*, T);    private:    CLASS * instance;               // This is the instance on which the delegate method will be called, or NULL when delegate represents a static method.    Method method;                  // The delegate either holds a pointer to a class member method,    StaticMethod static_method;     // or to a static method.    public:    // Initializes Delegate as pointer to static or global method    inline Delegate(StaticMethod p_method)        :instance(NULL), static_method(p_method)    { }        // Initializes Delegate as pointer to class member instance method    inline Delegate(CLASS * p_instance, Method p_method)        :instance(p_instance), method(p_method)    {        if (p_instance == NULL)            throw InvalidArgumentException();    }        // Calls the function pointer stored in the Delegate object.    // p_sender = optional, usually pointer to object that raised the event    // p_event_args = arguments passed to event handler, are defined by the type of the event    inline void Invoke(void * p_sender, T p_event_args)    {        if (instance)            // the delegate represents an instance method (requires 'this' pointer)            return (instance->*method)(p_sender, p_event_args);        else            // the delegate represents a static method            return (*static_method)(p_sender, p_event_args);    }};            // The event class holds an array of Delegate objects which get invoked when the event is raised.// T = type of arg passed to the delegate method// Only delegates with the same T as the event's can be assigned to the event. template <class T> class Event{private:    Array< DelegateBase<T> * > delegates; // stores pointers to delegate objects    public:    inline Event()    { }    // Raises the event - invokes all assigned delegates    inline void Raise(void * p_sender, T p_event_args)    {        for (int i = 0; i < delegates.Count(); ++i)            delegates->Invoke(p_sender, p_event_args);    }        // Adds a delegate to be invoked with the event.    inline Event & operator += (DelegateBase<T> * p_delegate)    {        delegates.Add(p_delegate);        return * this;    }        inline Event & operator -= (DelegateBase<T> * p_delegate)    {        delegates.Remove(p_delegate);        return * this;    }};


Now short example of usage:


// Class that declares and produces some events.class Button{public:    struct ClickEventArgs // this is just to demonstrate event specialization by arg type    {        int x, y;    };        Event<ClickEventArgs> Clicked;  // this event passes the mouse position to its delegates    Event<int> KeyPressed;          // this event passes the key code to its delegates        void ProcessInput()    {        ClickEventArgs mouse_pos;        int key_code;        // read input from keyboard, mouse, process it        // ...        Clicked.Raise(this, mouse_pos);     // at some point Clicked event is raised to request button click processing        // ...        KeyPressed.Raise(this, key_code);   // at another point KeyPressed event is raised to request processing of the pressed key    }};// Class that consumes some events.class MyApplication{    Button *btn;        void Main()    {        // initialize GUI        btn = new Button();        // wire up event processing        btn->Clicked += new Delegate<MyApplication,ClickEventArgs>(this, &MyApplication::handle_btn_Clicked);        // note we just ignore the other event (no interest in key events in this case)    }        void handle_Clicked(void * p_sender, ClickEventArgs p_args)    {        // Handle button click...        // Now the handler has access to the button (event originator) via p_sender        // and to custom additional arguments via p_args.        printf("Button (%p) was clicked: x = %d, y = %d", p_sender, p_args.x, p_args.y);    }}


Note, the trick with the CLASS template argument is there just to allow for retrieving class member function pointers because they cannot be casted to a more general pointer (like void*). Otherwise the Delegate class would suffice with only the T template arg and there will be no need for the DelegateBase class.

I believe this design meets all your requirements except for:

Quote:- Extensible events and handlers. UI paint event does not know about graphics API, therefore I create matching d3d_paint_event, which has graphics device in its context, and handler for it.


which I don't think the event system is appropriate place to implement this. You'll rather need some rendering abstraction that could be used by your event handlers regardless of the rendering API.

Martin
Quote:Original post by Dinsdale
I believe this design meets all your requirements except for:

Quote:- Extensible events and handlers. UI paint event does not know about graphics API, therefore I create matching d3d_paint_event, which has graphics device in its context, and handler for it.


which I don't think the event system is appropriate place to implement this. You'll rather need some rendering abstraction that could be used by your event handlers regardless of the rendering API.

Martin

Yeah that is pretty much same conclusion I arrived to aswell.

Observer pattern is actually pretty good idea here and to use events as signal like objects, when order does not matter. When order matters, I prefer passing events through interfaces.

But yes, your code might inspire me to combine both ways.. something like:
class button_clicked_event{    // basically your event args};template<typename Event>class event_signal{public:    void insert_listener(event_signal_listener<Event>&);    void erase_listener(event_signal_listener<Event>&);    void raise(void * p_sender, Event const& e);};

This topic is closed to new replies.

Advertisement