5
\$\begingroup\$

I'm writing a set of simple widgets for a microcontroller application. I'd like to use the observer pattern to pass around events: when a button is clicked, when a timeout occurs - there are many uses which I'm currently implementing with very ugly glue code, with similar code occuring again and again.

The whole system is more or less static, i.e. widgets are created on startup, not dynamically. I'd like to get away with as little heap usage as possible, just to be able to tell (at compile time) if I'm running out of RAM (16 kB). This is the reason why I've not implemented any delegate destructor code. It would never be used while the system is running.

When an event occurs, a Signal is fired, calling all connected observers. These are delegates that call the actual free function or method on some object. Here's my current code:

#include <iostream>

#include <functional>
#include <list>

// Interface for delegates with the same set of arguments
template<typename... args>
class AbstractDelegate
{
  public:
    virtual void operator()(args...) = 0;
};

There are two concrete Delegate classes. One for non-static member functions and one for free functions:

// Concrete member function delegate that discards the function's return value
template<typename T, typename ReturnType, typename... args>
class ObjDelegate : public AbstractDelegate<args...>
{
  public:
    typedef ReturnType (T::*ObjMemFn)(args...);
    ObjDelegate(T& obj, ObjMemFn memFn)
      : obj_(obj),
      memFn_(memFn)
    {
    }
    void operator()(args... a)
    {
      (obj_.*memFn_)(a...);
    }
  private:
    T& obj_;
    ObjMemFn memFn_;
};

// Concrete function delegate that discards the function's return value
template<typename ReturnType, typename... args>
class FnDelegate : public AbstractDelegate<args...>
{
  public:
    FnDelegate(ReturnType (*fn)(args...))
      : fn_(fn)
    {
    }
    void operator()(args... a)
    {
      (*fn_)(a...);
    }
  private:
    ReturnType (*fn_)(args...);
};

Helper functions for Delegate construction:

// create a delegate, returned object can be auto'ed
template<typename T, typename ReturnType, typename... args>
ObjDelegate<T, ReturnType, args...> make_delegate(T& obj, ReturnType (T::*memFn)(args...))
{
  return ObjDelegate<T, ReturnType, args...>(obj, memFn);
}

// create a delegate, returned object can be auto'ed
template<typename ReturnType, typename... args>
FnDelegate<ReturnType, args...> make_delegate(ReturnType(*Fn)(args... a))
{
  return FnDelegate<ReturnType, args...>(Fn);
}

These two classes provide similar interfaces that I'd like to call when an event occurs:

class A
{
  public:
    void foo(int i)
    {
      std::cout << "A::foo(" << i << ")" << std::endl;
    }
    static int baz(int i) {std::cout << "A::baz(" << i << ")" << std::endl; return i;}
};

class B
{
  public:
    int bar(int i)
    {
      std::cout << "B::bar(" << i << "): " << i+1 << std::endl;
      return i;
    }
};

A free function should also work:

int foo(int i)
{
  std::cout << "foo(" << i << ")" << std::endl;
  return i;
}

The signal class is used to call a number of delegates. I've only included it here to complete the example, the implementation for the microcontroller application will be different:

template<typename... args> // just the event arguments, nothing more
class Signal
{
  public:
    typedef AbstractDelegate<args...>* delegate_p;
    void operator()(args... a) const
    {
      typename std::list<delegate_p>::const_iterator i = listeners_.begin();
      while(i != listeners_.end())
      {
        (**i)(a...);
        ++i;
      }
    }
    void connect(const delegate_p& p)
    {
      listeners_.push_back(p);
    }
  private:
    std::list<delegate_p> listeners_;
};

main creates a signal and adds observers to it:

int main()
{
  // void(int) member:
  A a;
  auto d0 = make_delegate(a, &A::foo);

  // int(int) member:
  B b;
  auto d1 = make_delegate(b, &B::bar);

  // int(int) free function:
  auto d2 = make_delegate(foo);

  // int(int) static member function:
  auto d3 = make_delegate(&A::baz)

  Signal<int> sig;
  sig.connect(&d0);
  sig.connect(&d1);
  sig.connect(&d2);
  sig.connect(&d3);
  sig(3);

  return 0;
}
\$\endgroup\$
6
\$\begingroup\$

Some tidbits about using C++11 features efficiently:

  • I find that the old typedef is rather hard to read for function pointers. You could refactor this line:

    typedef ReturnType (T::*ObjMemFn)(args...);
    

    into this one using a C++11 type alias:

    using ObjMemFn = ReturnType (T::*)(args...);
    
  • You can use curly braces instead of regular parenthesis to initilize the member variables in your constructor initialization lists. That will prevent implicit narrowing type conversions:

    ObjDelegate(T& obj, ObjMemFn memFn)
      : obj_{obj},
        memFn_{memFn}
    {}
    

    Be careful though, it does not work with some types, like reference types for example.

  • you can also use the new override specifier to explicitly tell the compiler that you do want to override a base class function. For example, ObjDelegate::operator() could be declared as:

    void operator()(args... a) override
    {
        // ...
    }
    

    While it is not necessary, it may help you to catch some subtle errors when the signature of the function you want to override in the base class is slightly different from the one you wrote in the derived class.

  • You can use list initialization to reduce the boilerplate in make_delegate: using a pair of curly braces in a return statement constructs and returns a value of the same type as the declared return type:

    template<typename ReturnType, typename... args>
    FnDelegate<ReturnType, args...> make_delegate(ReturnType(*Fn)(args... a))
    {
        return { Fn };
    }
    
  • Don't hesitate to use auto to deduce types instead of writing overly long types. Some same that the idiom should be "Almost Always auto". For example, instead of:

    typename std::list<delegate_p>::const_iterator i = listeners_.begin();
    

    write:

    auto i = listeners_.cbegin();
    

    Note that I used cbegin instead of begin to make sure that the deduced type would be const. If two overload of a function are available (const and non-const), the one without the const specifier will be choosed by the compiler.

  • And the usual one, that I almost forgot (because it's not C++11 specific): you do not need to write return 0; at the end of your main. If your main ends without having encountered a return statement, the compiler will automagically add return 0; at the end of main for you :)

\$\endgroup\$
  • \$\begingroup\$ That bit near the end about using 'cbegin' is confusing; the code shows begin, the first part of the text mentions cbegin, then the rest of the paragraph implies you really wanted begin. \$\endgroup\$ – Niall C. Apr 13 '14 at 22:46
  • \$\begingroup\$ I have accepted this answer because it helped to improve my code. Not all suggestions worked, though - I think this is the compiler's fault. I'll add my updated code including any errors related to the suggested improvements as an answer. \$\endgroup\$ – Christoph Apr 16 '14 at 21:14
  • 3
    \$\begingroup\$ @Christoph beware, the suggested code does not use perfect forwarding or universal references. Instead it creates an operator() that requires r-value reference variants of each type that was specified for the Signal class's template. For universal references, the compiler must be resolving the types at its call, so operator() itself must be a templated method. Alternately, if the initial types are correct, remove the && from operator()'s declaration. \$\endgroup\$ – Michael Urman Apr 27 '14 at 12:27
  • 2
    \$\begingroup\$ @Christoph It's best explained by Scott Meyers (see links in stackoverflow.com/a/14381110/89999). The short version is that by reusing args from template <...args> class Signal in the declaration operator()(args&&... a), the pack gets expanded with && added: Signal<int> gives operator()(int&& a). This syntax thus requires an r-value reference, such as a temporary variable, or one like std::move(my_int). Making it template <typename... Ts> void operator()(Ts&&... ts) allows resolving either int& or int&&; paired with std::forward it does the right thing. \$\endgroup\$ – Michael Urman Apr 27 '14 at 12:54
  • 2
    \$\begingroup\$ @Morwenn Totally understood. I missed the flaw until I saw Christoph's follow-up question. I would thus consider reserving your guidance to perfect forwarding to cases of performance-critical code and where already deducing a variadic template. In Christoph's case, in order to use perfect forwarding, you either feel like you're giving up type safety by re-templating (but aren't once it resolves the ObjDelegate or FnDelegate call), or you introduce errors like this by not re-templating. \$\endgroup\$ – Michael Urman Apr 27 '14 at 13:07
2
\$\begingroup\$

Below is my signals and connections header file. I'm only beginning to use it in my actual application, but it seems to provide what I want.

The one thing that didn't really work as suggested by Morween was using a brace-enclosed initializer list for reference members (see ObjDelegate(T& obj, ObjMemFn memFn)). I think the same issue is discussed in this question.

gcc -v:

compiler: gcc version 4.8.3 20140228 (release) [ARM/embedded-4_8-branch revision 208322]).

The make_delegate(...) functions had to go in favour of a Connection class and free connect functions:

#ifndef _SIGNALS_H_
#define _SIGNALS_H_

#include <utility>

/** Interface for delegates with a specific set of arguments **/
template<typename... args>
class AbstractDelegate
{
  public:
    virtual void operator()(args&&...) const = 0;
    virtual ~AbstractDelegate() {}
};

/** Concrete member function delegate that discards the function's return value **/
template<typename T, typename ReturnType, typename... args>
class ObjDelegate : public AbstractDelegate<args...>
{
  public:
    /** member function typedef **/
    using ObjMemFn = ReturnType (T::*)(args...);

    /** constructor **/
    ObjDelegate(T& obj, ObjMemFn memFn)
//      : obj_{obj}, //error: invalid initialization of non-const reference of type 'Watch&' from an rvalue of type '<brace-enclosed initializer list>'
      : obj_(obj),
      memFn_{memFn} // here the brace-enclosed list works, probably because memFn is _not_ a reference
    {
    }

    /** call operator that calls the stored function on the stored object **/
    void operator()(args&&... a) const override
    {
      (obj_.*memFn_)(std::forward<args>(a)...);
    }

  private:
    /** reference to the object **/
    T& obj_;
    /** member function pointer **/
    const ObjMemFn memFn_;
};

/** Concrete function delegate that discards the function's return value **/
template<typename ReturnType, typename... args>
class FnDelegate : public AbstractDelegate<args...>
{
  public:
    /** member function typedef **/
    using Fn = ReturnType(*)(args...);

    /** constructor **/
    FnDelegate(Fn fn)
      : fn_{fn}
    {
    }

    /** call operator that calls the stored function **/
    void operator()(args&&... a) const override
    {
      (*fn_)(std::forward<args>(a)...);
    }

  private:
    /** function pointer **/
    const Fn fn_;
};

/** forward declaration **/
template<typename... args>
class Connection;

/** Signal class that can be connected to**/
template<typename... args>
class Signal
{
  public:
    /** connection pointer typedef **/
    typedef Connection<args...>* connection_p;

    /** constructor **/
    Signal()
      : connections_(NULL)
      {
      }

    /** call operator that calls all connected delegates.
      The most recently connected delegate will be called first **/
    void operator()(args&&... a) const
    {
      auto c = connections_;
      while(c != NULL)
      {
        (c->delegate())(std::forward<args>(a)...);
        c = c->next();
      }
    }

    /** connect to this signal **/
    void connect(connection_p p)
    {
      p->next_ = connections_;
      connections_ = p;
      p->signal_ = this;
    }

    /** disconnect from this signal.
      Invalidates the connection's signal pointer
      and removes the connection from the list **/
    void disconnect(connection_p conn)
    {
      // find connection and remove it from the list
      connection_p c = connections_;
      if (c == conn)
      {
        connections_ = connections_->next();
        conn->next_ = NULL;
        conn->signal_ = NULL;
        return;
      }
      while(c != NULL)
      {
        if (c->next() == conn)
        {
          c->next_ = conn->next();
          conn->next_ = NULL;
          conn->signal_ = NULL;
          return;
        }
        c = c->next();
      }
    }

    /** destructor. disconnects all connections **/
    ~Signal()
    {
      connection_p p = connections_;
      while(p != NULL)
      {
        connection_p n = p->next();
        disconnect(p);
        p = n;
      }
    }

  private:
    connection_p connections_;
};

/** connection class that can be connected to a signal **/
template<typename... args>
class Connection
{
  public:
    /** template constructor for non-static member functions.
      allocates a new delegate on the heap **/
    template<typename T, typename ReturnType>
    Connection(Signal<args...>* signal, T& obj, ReturnType (T::*memFn)(args...))
      : delegate_(new ObjDelegate<T, ReturnType, args...>(obj, memFn)),
      signal_(NULL),
      next_(NULL)
    {
      signal->connect(this);
    }

    /** template constructor for static member functions and free functions.
      allocates a new delegate on the heap **/
    template<typename ReturnType>
    Connection(Signal<args...>* signal, ReturnType (*Fn)(args...))
      : delegate_(new FnDelegate<ReturnType, args...>(Fn)),
      signal_(NULL),
      next_(NULL)
    {
      signal->connect(this);
    }

    /** get reference to this connection's delegate **/
    AbstractDelegate<args...>& delegate() const
    {
      return *delegate_;
    }

    /** get pointer to next connection in the signal's list **/
    Connection* next() const
    {
      return next_;
    }

    /** is this connection connected to a valid signal? **/
    bool connected() const
    {
      return (signal_ != NULL);
    }

    /** desctructor. If the signal is still alive, disconnects from it **/
    ~Connection()
    {
      if (signal_ != NULL)
      {
        signal_->disconnect(this);
      }
      delete delegate_;
    }

    friend class Signal<args...>;
  private:
    AbstractDelegate<args...>* delegate_;
    Signal<args...>* signal_;
    Connection* next_;
};

/** free connect function: creates a connection (non-static member function) on the heap
  that can be used anonymously **/
template<typename T, typename ReturnType, typename... args>
Connection<args...>* connect(Signal<args...>* signal, T& obj, ReturnType (T::*memFn)(args...))
{
  return new Connection<args...>(signal, obj, memFn);
}

/** free connect function: creates a connection (static member or free function) on the heap
  that can be used anonymously **/
template<typename ReturnType, typename... args>
Connection<args...>* connect(Signal<args...>* signal, ReturnType (*fn)(args...))
{
  return new Connection<args...>(signal, fn);
}

#endif // _SIGNALS_H_

I am still not totally satisfied with this, because the function pointers stored in the delegates don't necessarily need to end up on the heap. I want to use as little heap space as possible.

\$\endgroup\$
  • \$\begingroup\$ I thought I had tested all the constructors to make sure that the braces initialization worked. I might have overlooked something :/ \$\endgroup\$ – Morwenn Apr 16 '14 at 22:02
  • \$\begingroup\$ Anyway, instead of posting the modified code as an answer, you should post another question, stating that it is a follow-up question, see this topic for more information :) \$\endgroup\$ – Morwenn Apr 16 '14 at 22:03
  • \$\begingroup\$ Alternatively, you could have posted as a regular answer what can be improved and then opened a new question with the whole new code since you are not totally satisfied. \$\endgroup\$ – Morwenn Apr 16 '14 at 22:05
  • \$\begingroup\$ Two days ago I have commented the accepted answer in the topic you linked to and my impression is that posting the end result of one iteration is ok. I'm not planning to add more answers and code here, but open a new question instead when I decide that I really need to improve the code above. \$\endgroup\$ – Christoph Apr 16 '14 at 22:12
  • 1
    \$\begingroup\$ _SIGNALS_H_ is a reserved name, using it for your own header guards is not allowed. SIGNALS_H is a perfectly good name to use instead of copying what you're seen in other headers. \$\endgroup\$ – Jonathan Wakely Apr 27 '14 at 13:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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