2
\$\begingroup\$

I wanted to allow components of my program to communicate without the components knowing about each other, thus keeping coupling low. So I devised some template classes that implement the observer pattern and allow classes to send messages of any type, providing they inherit from a message interface base class, and a message listener base class, paramaterized by the same message type (please feel free to correct my terminology here!)

Please ignore references to the LOG macro; it's just something I use to debug.

#ifndef MESSAGING_HPP
#define MESSAGING_HPP
#define LOG(...)

#include <vector>

namespace messaging{

template<typename T>
class IMessageListener;

template <typename T>
class IMessageContext {
    friend class IMessageListener<T>;

    template <typename U>
    friend void send(IMessageContext<U>* ptrContext, const U& _msg);

    template <typename U>
    friend void attach(IMessageContext<U>* ptrContext, IMessageListener<U>* ptrListener);

    template <typename U>
    friend void detach(IMessageContext<U>* ptrContext, IMessageListener<U>* ptrListener);

    std::vector<IMessageListener<T>* > listeners;

    void attachListener( IMessageListener<T>* _listener);
    void detachListener( IMessageListener<T>* _listener);
    void dispatch(const T& _msg);
public:
    ~IMessageContext();
};

template <typename T>
void IMessageContext<T>::attachListener( IMessageListener<T>* _listener)
{
    LOG(__PRETTY_FUNCTION__);
    listeners.push_back(_listener);
}

template <typename T>
void IMessageContext<T>::detachListener( IMessageListener<T>* _listener)
{
    LOG(__PRETTY_FUNCTION__);
    listeners.erase(std::remove(listeners.begin(), listeners.end(), _listener), listeners.end());
}

template <typename T>
void IMessageContext<T>::dispatch(const T& _msg)
{
    LOG(__PRETTY_FUNCTION__);
    typename std::vector<IMessageListener<T>* >::iterator i;
    for(i = listeners.begin(); i != listeners.end(); ++i){
        (*i)->handleMessage(_msg);
    }
}

template <typename T>
IMessageContext<T>::~IMessageContext()
{
    //detach from all listeners so they dont try to detach from this context when they are destructed
    LOG(__PRETTY_FUNCTION__);
    typename std::vector<IMessageListener<T>* >::iterator i;
    for(i = listeners.begin(); i != listeners.end(); ++i){
        (*i)->messaging::template IMessageListener<T>::detachContext(this);
    }
}

template<typename T>
class IMessageListener {
    friend class IMessageContext<T>;

    template <typename U>
    friend void attach(IMessageContext<U>* ptrContext, IMessageListener<U>* ptrListener);

    template <typename U>
    friend void detach(IMessageContext<U>* ptrContext, IMessageListener<U>* ptrListener);

    std::vector<IMessageContext<T>* > contexts;

    void attachContext( IMessageContext<T>* _listener);
    void detachContext( IMessageContext<T>* _listener);
    virtual void handleMessage(const T&)  = 0;
public:
    virtual ~IMessageListener();

};

template <typename T>
void IMessageListener<T>::attachContext( IMessageContext<T>* _context)
{
    LOG(__PRETTY_FUNCTION__);
    contexts.push_back(_context);
}

template <typename T>
void IMessageListener<T>::detachContext( IMessageContext<T>* _context)
{
    LOG(__PRETTY_FUNCTION__);
    contexts.erase(std::remove(contexts.begin(), contexts.end(), _context), contexts.end());
}

template <typename T>
IMessageListener<T>::~IMessageListener()
{
    //detach from all contexts so they dont try to message missing listener
    LOG(__PRETTY_FUNCTION__);
    typename std::vector<IMessageContext<T>* >::iterator i;
    for(i = contexts.begin(); i != contexts.end(); ++i){
        (*i)->messaging::template IMessageContext<T>::detachListener(this);
    }
}

// The real API

template <typename T>
void send(IMessageContext<T>* ptrContext, const T& _msg)
{
    LOG(__PRETTY_FUNCTION__);
    ptrContext->template IMessageContext<T>::dispatch(_msg);
}

template <typename T>
void attach(IMessageContext<T>* ptrContext, IMessageListener<T>* ptrListener)
{
    LOG(__PRETTY_FUNCTION__);
    ptrContext->template IMessageContext<T>::attachListener(ptrListener);
    ptrListener->template IMessageListener<T>::attachContext(ptrContext);
}

template <typename T>
void detach(IMessageContext<T>* ptrContext, IMessageListener<T>* ptrListener)
{
    LOG(__PRETTY_FUNCTION__);
    ptrContext->template IMessageContext<T>::detachListener(ptrListener);
    ptrListener->template IMessageListener<T>::detachContext(ptrContext);
}

} //namespace messaging

#endif // MESSAGING_HPP

And an example of usage:

#include <iostream>
#include "messaging.hpp"
#include <string>

using namespace std;

class context : public messaging::IMessageContext<int>,
        public messaging::IMessageContext<std::string>
{
public:
    void go(const int &i)
    {
        messaging::send<int>(this, i);
    }
    void go(const string &s)
    {
        messaging::send<std::string>(this, s);
    }
};

class listener : public messaging::IMessageListener<int>,
        public messaging::IMessageListener<string>
{
    virtual void handleMessage(const int &i){
        std::cout << i << std::endl;
    }
    virtual void handleMessage(const string &i) {
        std::cout << i << std::endl;
    }

public:
    virtual ~listener(){}
};

int main()
{
    context c;
    listener l;

    messaging::attach<int>(&c,&l);
    messaging::attach<std::string>(&c,&l);

    c.go(1234);
    c.go("this is a string message");

    return 0;
}

As I am still new to C++ and programming in general, I was hoping to find critique on method and style, as well as thoughts on usefulness and efficiency of the idea.

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

The big problem here is that you can very easily end up with dangling pointers due to lifetime issues. A really small example:

void trouble()
{
    context c;
    listener li;

    messaging::attach<int>(&c, &l);

    return c;
}

Can you spot the problem above? We've declared both context and listener on the stack. When you go to use the returned context with something like c.go(1234), you'll invoke undefined behavior.

The general way of solving this would be to allocate both the context and the listener as shared_ptrs. This also involves some trickyness due to the fact that shared_ptr is reference counted, and listeners should not keep the reference alive (which they will), thus necessitating the use of weak_ptr.

Edit: A bit more detail. In this case, since an IMessageContext actually stores a vector of IMessageListener<T>, it should own the pointers that are passed to it. This suggests you should be using std::unique_ptr<T>:

using owned_ptr = std::unique_ptr<IMessageListener<T>>;
std::vector<owend_ptr> listeners;

void attachListener( owned_ptr&& listener_);
void detachListener( owned_ptr&& listener-);
void dispatch(const T& _msg);

Your attachListener implementation would be similar, but would move the pointer in instead of pushing it back:

template <typename T>
void IMessageContext<T>::attachListener(owned_ptr&& listener)
{
    listeners.push_back(std::move(listener));
}

The benefit of this is that the memory occupied by the vector of listeners is automatically reclaimed when its parent object (in this case, an IMessageContext instance) has its destructor called.

This isn't a complete demonstration by any means, but I'd highly recommend you read up about memory ownership and smart pointers in C++, mainly std::unique_ptr and std::smart_ptr.


On a (really) advanced note, you can also do some cool (but tricky!) things with your context and listener classes. At the moment, these need to be explicitly derived from IListener<T> for each specific T. Using some magic, you could write this as follows:

template <typename Base, typename... Bases>
class context:
    public messaging::IMessaging<Base>,
    public messaging::IMessaging<Bases>...
{
public:
    using messaging::IMessaging<Base>::send;
    using context<Bases...>::send;

    template <typename T>
    void go(const T& t)
    {
        send<T>(this, t);
    }
};

template <typename Base>
class context
  : public messaging::IMessaging<Base>
{
public:
    template <typename T>
    void go(const T& t)
    {
        send<T>(this, t);
    }
};

And similarly for listener. This crazy bit of hackery would allow you to declare your context class to receive whatever argument types it wants:

context<std::string, int> c;
listener<std::string, int> li;

This will effectively derive the context from each IMeessaging<T> for each T in the list of template arguments supplied to context/listener, and then forward to the correct send function.

\$\endgroup\$
2
  • \$\begingroup\$ Could the dangling pointer issue be resolved by having a copy constructor which updates the pointer in all the attached contexts/listeners? The second part of what you said is very cool, going to have to spend some time to understand whats going on though.. Thanks for taking the time! \$\endgroup\$
    – Rag
    Commented Sep 10, 2014 at 7:48
  • \$\begingroup\$ @Rag See my update; I've gone int a little bit more detail on this. Memory management in C++ with complex structures like this can take some getting used to, and I'd highly recommend you do a bit of reading about so called "smart pointers" to understand the problems they solve. \$\endgroup\$
    – Yuushi
    Commented Sep 10, 2014 at 8:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.