0

O wise interwebs

We have an impasse between two colleagues that we could use your help resolving in the proper C++ way. Basically we have a set of utility classes, two of which are a Mutex and SpinLock class which both have the following abridged interface:

class Mutex {
public:
    Mutex();
    ~Mutex();
    void Lock();
    void Unlock();
    // ...
};

Obviously this is similar to, but differently-cased than the BasicLockable concept used by std::lock_guard, so we want something similar (assume that the Mutex class is immutable in this example; we cannot add the BasicLockable concept to it). Also not all of our compilers for supported platforms are fully c++11 featured, so we cannot just use vanilla c++11 support.

One school of thought is the following implementation of a generic guard class which can be inherited to provide a generic guard class and inherit from it to create a lock-guard class:

template<class T, void (T::*EnterFn)(), void (T::*ExitFn)()>
class Guard
{
public: // copy constructor deleting omitted for brevity
    Guard( T *lock ) : m_lock(lock) { (m_lock->*EnterFn)(); }
    ~Guard();                       { (m_lock->*ExitFn)(); }
private:
    T *m_lock;
};

template<class T>
class LockGuard : public Guard<T, &T::Lock, &T::Unlock>
{
public:
    LockGuard(const T* lock) : Guard<T, &T::Lock, &T::Unlock>(lock) {}
};

The other school of thought is to just implement a simple lockguard:

template<class T>
class LockGuard {
    T* m_lockable;
public:
    LockGuard(const T* lockable) : m_lockable(lockable) { lockable->Lock(); }
    ~LockGuard() { m_lockable->Unlock(); }
};

Which implementation would you choose and why? What is the most proper C++(03, 11, 14, 17) way of implementing it? Is there any inherent value to having a generic Guard class as described above?

autenil
  • 11
  • 1
  • 3
  • Why do you need that beyond of the c++ standard facilities? – πάντα ῥεῖ Mar 06 '17 at 06:31
  • Welcome to Stack Overflow. Please take the time to read [The Tour](http://stackoverflow.com/tour) and refer to the material from the [Help Center](http://stackoverflow.com/help/asking) what and how you can ask here. – πάντα ῥεῖ Mar 06 '17 at 06:50
  • @πάνταῥεῖ we have an older codebase that we're supporting that contains a lot of pre-c++11 code. We already have a Mutex class. This question is really of the two options presented above, which is the more correct option, or is there a third (unmentioned) option that would be better? – autenil Mar 06 '17 at 08:06

3 Answers3

1

I would not want to use method pointers.

Personally, I'd want to move towards the C++11 standard tools as much as possible. So I'd write an adapter.

template<class T>
struct lock_adapter {
  T* t = nullptr;
  void lock() { t->Lock(); }
  void unlock() { t->Unlock(); }
  lock_adapter( T& tin ):t(std::addressof(tin)) {}
  // default some stuff if you like
};

template<class T>
struct adapted_unique_lock:
  private lock_adapter<T>,
  std::unique_lock< lock_adapter<T> >
{
  template<class...Args>
  adapted_unique_lock(T& t, Args&&...):
    lock_adapter<T>(t),
    std::unique_lock< lock_adapter<T> >( *this, std::forward<Args>(args)... )
  {}
  adapted_unique_lock(adapted_unique_lock&&)=delete; // sadly
  friend void swap( adapted_unique_lock&, adapted_unique_lock& ) = delete; // ditto
};

now adapted_unique_lock has a restricted set of functionality from a std::unique_lock.

It cannot be moved, as the unique_lock holds a pointer to this inside its implementation and does not reseat it.

Note that the richness of the entire unique_lock constructor set is available.

Functions that return adapted unique locks must store their return values in something like auto&& references until end of scope, and you cannot return them through chains until C++17.

But any code using adapted_unique_lock can be swapped to use unique_lock once T has been changed to support .lock() and .unlock(). This moves your code base towards being more standard C++11, rather than bespoke.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • This is an interesting solution, however I have changed he original question to remove c++11 as a possibility. Given that we need a c++03 solution, what would you recommend? – autenil Mar 06 '17 at 16:25
1

I'll answer the question in your title since nobody else answered it.

According to the docs, lock_guard works on any type that "meets the BasicLockable requirements". BasicLockable only requires two methods, lock() and unlock().

In order to make lock_guard work with a custom library, you need to either add lock() and unlock() methods to the library's mutex class, or wrap it in another class that has lock() and unlock() methods.

serg06
  • 2,027
  • 1
  • 18
  • 26
0

You should use those: std::mutex, std::shared_lock, std::unique_lock, std::timed_mutex, std::shared_mutex, std::recursive_mutex, std::shared_timed_mutex, std::recursive_timed_mutex, std::lock_guard if you actually have C++11 compiler. Otherwise it is more complex, and tag c++11 on question should be removed.

You can't implement spinlock or mutex with C\C++ means, you would need add assembler or intrinsic code - making it non-portable, unless you implement it for each platform - and with many x86-64 C++11 and later compilers you can't do inline assembler.

The main problem you would run into, if you use inherent locking logic is that objects behind mutexes or spinlocks are uncopyable. As soon as you copy it or if you copy defended variable, it stops being locked. Objects that are implementing mutex mechanics are uncopyable too.

Swift - Friday Pie
  • 12,777
  • 2
  • 19
  • 42
  • You forgot [std::lock_guard](http://en.cppreference.com/w/cpp/thread/lock_guard) ;-) – Jesper Juhl Mar 06 '17 at 08:02
  • Hi Swift, thanks for the input. We have an older codebase that has a lot of pre-c++11 code; that is what we are trying to support in c++11-like ways. I understand the uncopyable nature of mutexes and spin locks. In one of the examples I gave above there is a comment about the copy constructor deleting being omitted for brevity. The implication is that our version of a lock guard would require deleting the copy constructor. The question is really about which example presented above is the best solution for us and why. – autenil Mar 06 '17 at 08:09
  • @user1450686 I'm dealing with same legacy code situation at work (using alternatives between Qt's QMutex, C++11 mutex and old assembler implementation). Second approach is way easier to implement, in fact, isn't that just a std::lock_guard ?. In fact we had a class template of that implement lock\unlock procedures for that variable, so changes were even easier. – Swift - Friday Pie Mar 06 '17 at 08:16
  • @Jesper Juhl I thought about it twice, but forgot to insert. It's actually the second approach OP asks about – Swift - Friday Pie Mar 06 '17 at 08:17
  • I removed c++11 as not all of our platforms support it. We already have a platform independent interface (abridged version shown above) for Mutex and SpinLock. – autenil Mar 06 '17 at 16:24