jeudi 20 novembre 2014

Signal / slots implementation using C++11 variadic templates

Hello and welcome on this blog :)

You'll be able to follow the development of my various projects related to computer graphics as well as occasional ramblings about quantum physics. So let's get started straight away :)

One of my projects is a 3D graphics engine oriented towards non-interactive realtime animations - demos. For more informations about that, just google Demoscene, or go to pouet.net or demozoo.org.

Introduction


DemoKit uses C++11 extensively, and the feature I'll be talking about in this post is called "variadic templates".

In C++, you can use templates to abstract a type when writing a class. You might already be familiar with these because that's the way the STL container works. Let's take an example :

template<typename T>
class Foo
{
public:
    Foo(const T& t) : t_(t) {}
    T GetT() const { return t_; }

private:
    T t_;
};

Here, the Foo class is a very simple container that can be of any type. For instance,

Foo<int> f(42);

is an instance of the Foo class using int as its template parameter, meaning that everytime the type T is seen in Foo, it is replaced with int.

This also means that Foo<int> is not the same class as Foo<float>, because the template parameter alters the implementation of the class itself.
I won't get into details about template classes as it's outside the scope of this article, but just note that it also is possible to specify different implementations of a template class depending on the template parameter - it's called explicit template specialization.


Now, you might also be familiar with the concept of variadic functions, which allows you to pass as many arguments as you want to to a function. printf is an example.

Signals and slots


Those of you that have used Qt should know their system of signals/slots.
A signal is a fake function for which you do not define a body. Instead, you plug one or more slots to it, so that when you do call the signal (this is also called emitting the signal), what you actually do is call the slots that have been connected before.
What's even more interesting is that the slot does not have to be a static function - it can also be an object's member function.

Qt imposes the usage of their moc - meta-object compiler - to use these, though, and C++11 enables a very simple way to implement signals/slots - variadic templates.

Variadic templates


C++11 introduces a feature that kind of mixes variadic functions with templates - somewhat logically, it's called variadic templates. The principle is to allow someone to pass an indefinite number of template arguments. For example, Foo<int> and Foo<bool, float, std::string> could both be legal using variadic templates.

We can easily implement a simple signal using those variadic templates :

template<typename... Args> class Signal
{
public:
    typedef void(*FunctionPointer)(Args...);

    Signal() {}

    void Connect(FunctionPointer fptr)
    {
        ptrs_.push_back(fptr);
    }

    void emit(Args... args)
    {
        for(FunctionPointer& fp : ptrs_)
            fp(args...);
    }

    void operator()(Args... args)
    {
        emit(args...);
    }

private:
    std::vector<FunctionPointer> ptrs_;
};


A few words on this before moving on. What we are passing as template parameter is the list of arguments that will be passed to the function pointer. Also, I'm using a simple function pointer here... More on that later.

Now you could do this :

void Foo(int x, float y) { /* ... */ }
void Bar(int x, float y) { /* ... */ }


int main(int argc, char** argv)
{
    Signal<int, float> s;
    s.Connect(Foo);    s.Connect(Bar);
    s(42, 13.37f); // Calls both Foo(42, 13.37f) and Bar(42, 13.37f)
    s.emit(42, 13.37f); // Does the same
}

Of course this example isn't quite useful but it does illustrate the point. Trying to pass to s a function that does not take int and float, in that order, as arguments would result in a compile error.

STL function objects


There still is room for improvement, though. We'd need to be able to use anonymous functions, as well as connect member methods. Let's rewrite that class !

template<typename... Args> class Signal
{
public:
    Signal() {}

    void Connect(std::function<Foo(Args...)> fptr)
    {
        ptrs_.push_back(fptr);
    }

    void operator()(Args... args)
    {
        for(std::function<Foo(Args...)>& fp : ptrs_)
            fp(args);
    }

private:
    std::vector< std::function<Foo(Args...)> > ptrs_;
};


Now using an anonymous function would work too :

int main(int argc, char** argv)
{
    Signal<int, float> s;
    s.Connect([](int i, float f) {
        /* ... */
    });
    s(42, 13.37f);
}

We still have a last challenge to work out. Now we want to be able to call a member method on a class instance. To do that, we'll need some kind of proxy that will pass back the this parameter, effectively making it possible to call a member method :

template<class O, typename R, typename ... A>
std;;function<R(A...)> Bind(O* o, R(O::*f)(A...))
{
    return [=](A... args) { return (o->*f)(args...); };
}

Let's analyze this obscure piece of code =)
This is a function that, given :
  • an object instance of type O*,
  • a pointer to a member function that takes A as parameters and returns R
returns a function object calling the member function on the object O. But let's take an example :

class Foo
{
public:
    Foo() {}
    void BarMethod(int i, float f) { /* ... */ }
};

Foo* f = new Foo();
Signal<int, float> s;
s.Connect(Bind(f, &Foo::BarMethod));

s(42, 13.37f); // Calls f->BarMethod(42, 13.37f);

Conclusion


We have been able to construct a Signal class which is able to store connections to either anonymous functions, static functions and, with the help of the Bind function, to any member function. The slot definition is here implicit, any function or method can be a slot.

You might have noticed that there's no definition for a Signal with no argument. You'd need to use explicit template specialization to define a Signal<void> ; this is left as an exercise to the reader.

I hope this article isn't too dull to read - next stop is a policy-based variant type. :)

2 commentaires:

  1. « it also is possible to specify different implementations of a template class depending on the template parameter - it's called template specialization. »

    That should be called explicit template specialization. Template specialization is always performed – otherwise you don’t get anything. std::vector implies a template specialization :)

    RépondreSupprimer