Question

I've written the following implementation for a generic signal/slot system:

template< typename... Args >
class Signal : NonCopyable
{
public:

    typedef std::function< void (Args...) > Delegate;

    void connect(const Delegate& delegate);
    void operator ()(Args&&... args) const;

private:

    std::list< Delegate > _delegates;
};

template< typename... Args >
void Signal< Args... >::connect(const Delegate& delegate)
{
    _delegates.push_front(delegate);
}

template< typename... Args >
void Signal< Args... >::operator ()(Args&&... args) const
{
    for (const Delegate& delegate : _delegates)
        delegate(std::forward< Args >(args)...);
}

Afterwards, I tested my class using the following simple cases:

Signal< int > signal;

// Case 1
signal(0);

//Case 2
int i(0);
signal(i);

Case 1 compiles without issue. Case 2, on the other hand, generates the following error under GCC 4.7.2:

/home/pmjobin/Workspace/main.cpp:1196:10: error: cannot bind ‘int’ lvalue to ‘int&&’
/home/pmjobin/Workspace/main.cpp:82:6: error:   initializing argument 1 of ‘void Signal<Args>::operator()(Args&& ...) const [with Args = {int}]’

I understand the issue has something to do with perfect-forwarding (and my misunderstanding of the latter). However, I've inspired myself from the std::make_shared() & std::make_tuple() implementations and I fail to see any difference in the way I forward the variadic arguments to the delegates. The only notable difference being that both make_shared() and make_tuple() are function templates rather than a class template such as the Signal implementation above.

- EDIT -

In response to the various comments, here is a new version of the Signal class implementation which doesn't suffer from the aforementioned issues. Additionally, it is now possible to disconnect delegates using an opaque token returned by the connect function. The result might not be as flexible and powerful as other implementations out there (such as boost::signal), but at least, it has the benefit of being simple and lightweight.

template< typename Signature >
class Signal : NonCopyable
{
public:

    typedef std::function< Signature > Delegate;

    class DisconnectionToken
    {
        DisconnectionToken(typename std::list< Delegate >::iterator it)
            : _it(it)
        {}

        typename std::list< Delegate >::iterator _it;

        friend class Signal;
    };

    DisconnectionToken connect(const Delegate& delegate);
    void disconnect(DisconnectionToken& token);

    template< typename... Args >
    void operator ()(Args&&... args) const;

private:

    std::list< Delegate > _delegates;
};

template< typename Signature >
typename Signal< Signature >::DisconnectionToken Signal< Signature >::connect(const Delegate& delegate)
{
    _delegates.push_front(delegate);
    return DisconnectionToken(_delegates.begin());
}

template< typename Signature >
void Signal< Signature >::disconnect(DisconnectionToken& token)
{
    if (token._it != _delegates.end())
    {
        _delegates.erase(token._it);
        token._it = _delegates.end();
    }
}

template< typename Signature >
template< typename... Args >
void Signal< Signature >::operator ()(Args&&... args) const
{
    for (const Delegate& delegate : _delegates)
        delegate(std::forward< Args >(args)...);
}
Was it helpful?

Solution

The problem is that you're explicitly specifying the template parameter as an int. As Nawaz mentioned, Args&&... gets expanded into int&& and you can't bind an lvalue to an rvalue reference.

The reason perfect forwarding works is that when you call a function (for example) without specifying the template arguments, they get deduced to either & or && and then the references collapse (read about reference collapsing if you don't know what that is). You do explicitly specify it though, so you inhibit reference collapsing and screw it all up.

One thing you could do is give the operator() it's own list of template arguments:

template<typename... Args2>
void operator ()(Args2&&... args) const;

...

template< typename... Args >
template< typename... Args2 >
void Signal< Args... >::operator ()(Args2&&... args) const
{
    for (const Delegate& delegate : _delegates)
        delegate(std::forward< Args2 >(args)...);
}

This way you can let reference collapsing take care of it for you.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top