Blag

He's not dead, he's resting

Implementing Active Objects using Smart Pointers

An Active Object is, essentially, a threaded design pattern where an object is always called from the same thread, and where a proxy provides synchronised access to that object. It’s useful in cases where there’s little or no parallelism possible due to the underlying object’s state, and where implementing manual locking would be a nuisance.

The problem in C++, generally, is implementing the proxy. If the underlying object’s class has twenty methods, you have to implement twenty trivial wrapper methods for the proxy that obtain the lock and then forward. Whilst not difficult to do, it’s rather tedious.

But what if the proxy is a smart pointer? Then you could do proxy->method() for any method the underlying class has, and you wouldn’t have to worry about writing wrapper methods. The question then is how to write Proxy<UnderlyingClass>::operator-> ().

It can’t simply return the underlying instance, since it needs to do locking. And it can’t obtain a lock and then return the underlying instance, since the lock would never be released.

But all is not lost. The standard has some rather interesting wording:

An expression x->m is interpreted as (x.operator->())->m for a class object x of type T if T::operator-> () exists and if the operator is selected as the best match function by the overload resolution mechanism.

Usually, implementations of operator-> () simply return SomeType *. But with the way the standard is worded, they could instead return a second class instance, so long as that new class has operator-> () defined.

So we make Proxy<UnderlyingClass>::operator-> () return a temporary that, upon construction, obtains the shared lock, and upon destruction releases it. Then we rely upon the temporary’s operator-> () returning a pointer to the underlying object to get the actual method call.

Except… It’s not that simple. With current C++, the temporary has to be copyable (even though the compiler optimises out the copy), but typically mutex locks are noncopyable. With C++0x we’ll be able to return by rvalue reference, but until then we have to store the lock in a shared pointer rather than directly.

We’re going to start making use of this in Paludis to cut down on boilerplate code. A working implementation follows. We have a couple of refinements: the class is called ActiveObjectPtr, and it’s parameterised by a pointer to the underlying class (we’ll see why in a later post — generally it’ll just be a std::tr1::shared_ptr, but leaving it as a parameter lets us compose smart pointer types).

template <typename T_>
class ActiveObjectPtr
{
    private:
        T_ _ptr;
        std::tr1::shared_ptr<Mutex> _mutex;

        class Deref
        {
            private:
                const ActiveObjectPtr * _ptr;
                std::tr1::shared_ptr<Lock> _lock;

            public:
                Deref(const ActiveObjectPtr * p) :
                    _ptr(p),
                    _lock(make_shared_ptr(new Lock(*p->_mutex)))
                {
                }

                const T_ & operator-> () const
                {
                    return _ptr->_ptr;
                }
        };

        friend class Deref;

    public:
        ActiveObjectPtr(const T_ & t) :
            _ptr(t),
            _mutex(new Mutex)
        {
        }

        ActiveObjectPtr(const ActiveObjectPtr & other) :
            _ptr(other._ptr),
            _mutex(other._mutex)
        {
        }

        ~ActiveObjectPtr()
        {
        }

        ActiveObjectPtr &
        operator= (const ActiveObjectPtr & other)
        {
            if (this != &other)
            {
                _ptr = other._ptr;
                _mutex = other._mutex;
            }
            return *this;
        }

        Deref operator-> () const
        {
            return Deref(this);
        }
};

At this stage, it’s not really a proper smart pointer. It doesn’t (and probably shouldn’t) have operator*, and it ignores the const issue. Handling these is left as an easy exercise for the reader.

Advertisements

2 responses to “Implementing Active Objects using Smart Pointers

  1. Pingback: On-Demand Loading using Smart Pointers « Ciaran McCreesh’s Blag

  2. pizer March 30, 2009 at 12:31 pm

    Nice!

    But in C++0x you don’t need to return an rvalue reference. I would ge even further and say that you should *never* return an rvalue reference — with the exception of std::move and std::forward.

    As far as I know C++0x will remove the CopyConstructible requirement for temporaries so it’ll work just fine if you return a non-copyable rvalue.

    Cheers!
    P

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s