mr-edd.co.uk :: horsing around with the C++ programming language

Nifty little lock class

[8th March 2012]

Happy new(?) year!

Ok, ok, so I haven't written in a while. I've been meaning to finish of the series on undo/redo for some time now. Hopefully I'll get that put to bed at some point, but in the meantime I'll try to get the ball rolling with something smaller.

If you've written any code that uses low-level threading primitives such as mutexes, you'll likely/hopefully be aware of the 'scoped-locking' idiom. In short, it looks something like this:

void some_function(mutex &m)
{
    lock lk(m); // calls m.acquire();
    // ...

    // lk's destructor calls m.release().
}

It ensures that a lock is always released, regardless of whether the scope is left via a return statement, or due to an exception. This is a nice way to statically ensure that you don't accidentally leave a mutex in an acquired state[1].

But it's quite often the case that a threading library will offer a selection of different mutex types to choose from. Each mutex will offer the same core interface: acquire() and release(). What typically happens, therefore, is that the lock class is becomes a template with the type of mutex as a template-parameter. This in turn means that the creation of the lock object can take a bit more typing than we might like:

void some_function(recursive_mutex &m)
{
    lock<recursive_mutex> lk(m);
    // ...
}

On its own that's not too bad, but any function that once took a reference to a lock object as a parameter may now have to become a template function in order to accept arbitrary lock types, possibly increasing compile times as a side-effect. This is the case with some of the methods on boost::condition_variable, for example.

Here's a simple but cunning technique for overcoming these problems:

namespace impl
{
    template<typename Mutex>
    void release(void *mutex)
    {
        assert(mutex);
        static_cast<Mutex *>(mutex)->release();
    }

} // impl

class lock : uncopyable
{
    public:
        template<typename Mutex>
        lock(Mutex &mtx) : 
            mtx_(&mtx),
            release_(&impl::release<Mutex>) 
        {
            mtx.acquire();
        }

        ~lock() { release_(mtx_); }

    private:
        void *mtx_;
        void (*release_)(void *);
};

Note that the lock class is not a template, but its constructor is. A function is 'made' in the initializer list via template instantiation. This function, impl::release<Mutex>, knows how to cast a void* to a Mutex* and call its release() method. Stashing a pointer to this function as a member variable and storing the address of the Mutex as an opaque pointer gives us everything we need to perform the release operation in the destructor.

Given that the lock class is not a template, we can now revert back to using the original syntax, regardless of the type of mutex we're given.

void some_function(recursive_mutex &m) // or could be a regular 'mutex', doesn't matter!
{
    lock lk(m);
    // ...
}

What we've done here is a basic form of type erasure, which is very useful for reducing compile-time dependencies.

You might have noticed that the same thing could have been achieved by creating an abstract_mutex interface with virtual acquire() and release() functions. This would also allow template functions taking arbitrary mutex objects to be re-written as non-template functions, but of course we'd then have to implement that interface a number of times to support all our mutex types.

Come down

Before I start riding too high on a wave of smugness, I should point out some problems. The first is that there's likely to be a small runtime penalty for the call through a function-pointer, though I'm betting that it wouldn't be anywhere near enough to care about in most cases, especially if significant compile-time savings can be made.

The second, more pressing problem, is that it's quite likely that the lock class should itself have some acquire() and release() functions. These are needed to implement condition variables that play nice with scoped-locking, for example.

To fix this, we could store another function pointer member, which casts-and-acquires the mutex identified by a void pointer, in much the same way that the existing function pointer casts-and-releases. But if we take a step back as this point, we'll see that we're incredibly close to manually constructing a virtual method table.

Pseudo-vtables

Let's run with that idea a bit further. Instead of storing multiple function pointers, we can instead store a single pointer to a static structure containing function pointers — exactly like a 'traditional' vtable.

namespace impl
{
    struct lock_vtable
    {
        void (*acquire)(void *);
        void (*release)(void *);
    };

    template<typename Mutex>
    struct lock_traits
    {
        static void acquire(void *mutex) { assert(mutex); static_cast<Mutex *>(mutex)->acquire(); }
        static void release(void *mutex) { assert(mutex); static_cast<Mutex *>(mutex)->release(); }

        static const lock_vtable vtable;
    };

    // Initializer for static vtable member of lock_traits<Mutex>
    template<typename Mutex>
    const lock_vtable lock_traits<Mutex>::vtable = 
    {
        &lock_traits<Mutex>::acquire,
        &lock_traits<Mutex>::release;
    };

} // impl

class lock : uncopyable
{
    public:
        template<typename Mutex>
        lock(Mutex &mtx) : 
            mtx_(&mtx),
            vt_(&impl::lock_traits<Mutex>::vtable)
        {
            vt_->acquire(mtx_);
        }

        ~lock() { vt_->release(mtx_); }

        void acquire() { vt_->acquire(mtx_); }
        void release() { vt_->release(mtx_); }

    private:
        void *mtx_;
        const impl::lock_vtable *vt_;
};

It's a little unorthodox, but for the compile-time-conscious developer it may be preferable it to having a cascade of template functions. I certainly prefer it to having a hierarchy of polymorphic classes; functionally, we're doing pretty much exactly the same thing, but with a single class.

I've used this pseudo-vtable technique in a handful of places now, so I think (hope!) it was worth presenting.

Footnotes
  1. Many will likely recognise this as an instance of the RAII idiom []

Comments

poindexter

[08/03/2012 at 14:20:55]

It's probably worth pointing out that if your compiler supports C++11's auto you could do something like this:

template< typename Mutex >
lock_class< Mutex > lock( Mutex m ) {
return lock_class< Mutex >( m );
}

auto lk = lock( m );

Edd

[24/03/2012 at 20:57:06]

That indeed solves the annoyance of having to type more than one might like for a lock object definition, but the larger problem of template explosion still remains.

(optional)
(optional)
(required, hint)

Links can be added like [this one -> http://www.mr-edd.co.uk], to my homepage.
Phrases and blocks of code can be enclosed in {{{triple braces}}}.
Any HTML markup will be escaped.