P0156R1
Mike Spertus, Symantec
mike_spertus@symantec.com
revision of P0156R0
2015-10-21
Audience: Library Working Group
The basic idea of this proposal is that std::lock_guard would benefit from being variadic to support multiple locks analogously to how std::lock does.
lock_guard is a very useful and widely used way to manage the lifetimes
        of lock ownership.
std::mutex mtx;
void f(){
  std::lock_guard<mutex> lck(mtx); // Mutex will be unlocked however scope is exited
  /* ... */
}
        
Unfortunately, if more than one lock needs to be required, life gets unnecessarily complicated
void swap(MyType const &l, MyType const &r)
{
    std::lock(l.mtx, r.mtx);
    std::lock_guard<std::mutex> llck(l.mtx, std::adopt_lock);
    std::lock_guard<std::mutex> rlck(r.mtx, std::adopt_lock);
    /* ... */
}
This is a lot more advanced and error-prone than the single lock case. For example,
you often see the deadlock-invitingvoid swap(MyType const &l, MyType const &r)
{
    std::lock_guard<std::mutex> llck(l.mtx);
    std::lock_guard<std::mutex> rlck(r.mtx);
    /* ... */
} or the exception-unsafe void swap(MyType const &l, MyType const &r)
{
    std::lock(l.mtx, r.mtx);
    /* ... */
    l.mtx.unlock();
    r.mtx.unlock();
}
    
These are exactly the kinds of complexities that are easily avoided by std::lock_guard in the single-lock case. Wouldn't it be great if std::lock_guard was variadic so it also worked with multiple locks?
This paper proposes a class scoped_lock that does exactly that.
void swap(MyType const &l, MyType const &r)
{
    std::scoped_lock lck(l.mtx, r.mtx);  // Leverages P0091R3, Template argument deduction for constructors
        /* ... */
} 
This can work in the presence of shared locking as well (just like std::lock does) as 
shown in the following example shared by Howard Hinnant#include <mutex>
#include <shared_mutex>
class X
{
    using Mutex = std::shared_timed_mutex;
    using ReadLock = std::shared_lock<Mutex>;
    mutable Mutex mut_;
    // more data
public:
    // ...
    X& operator=(const X& x)
    {
        if (this !=&x)
            ReadLock  rl(x.mut_, std::defer_lock);
            std::scoped_lock lck(mut_, rl);
            // assign data ...
        }
        return *this;
    }
    // ...
};
which improves clarity while eliminating .
We do not propose creating a &ldq is not moveable (which we suspect is the reason std::lock_guard didn't have a make function in the first place), because we believe the two-lock case is most important in practice, because we want to keep this proposal simple, and because something along the lines of P0091R3 may offer a more general solution that obviates the entire issue as shown in the example above.
Revert §30.4.2.1 [thread.lock.guard] to match its form in the C++14 standard ISO/IEC 14882:2014. Add a new section §30.4.2.x [thread.scoped.lock]as followstemplate <class... MutexTypes> class lock_guard; template <class... MutexTypes> class scoped_lock;
namespace std { template <class... MutexTypes> class scoped_lock { public: typedef Mutex mutex_type; // If MutexTypes... consists of the single type Mutex explicit scoped_lock(MutexTypes&... m); scoped_lock(MutexTypes&... m, adopt_lock_t); ~scoped_lock(); lock_guard(lock_guard const&) = delete; lock_guard& operator=(lock_guard const&) = delete; private: tuple<MutexTypes&...>> pm; // exposition only }; }An object of type scoped_lock controls the ownership of lockable objects within a scope. A scoped_lock object maintains ownership of lockable objects throughout the scoped_lock object's lifetime (3.8). The behavior of a program is undefined if the lockable objects referenced by pm do not exist for the entire lifetime of the scoped_lock object. When sizeof...(MutexTypes) is 1, the supplied Mutex type shall meet the BasicLockable requirements. Otherwise, each of the mutex types shall meet the Lockable requirements. (30.2.5.2).explicit scoped_lock(MutexTypes&... m);Requires: If a MutexTypes type is not a recursive mutex, the calling thread does not own the corresponding mutex element of m.
Effects:Initializes pm with tie(m...). Then if sizeof...(MutexTypes) is 0, no effects. Otherwise if sizeof...(MutexTypes) is 1, then m.lock(). Otherwise, then lock(m...).scoped_lock(MutexTypes&... m, adopt_lock_t);Requires: The calling thread owns all the mutexes in m.
Effects:Initializes pm with tie(m...).
Throws: Nothing.~scoped_lock();Effects: For all i in [0, sizeof...(MutexTypes)), get<i>(pm).unlock()
#include<mutex>
#include<tuple>
using std::tuple;
// Taken from http://stackoverflow.com/questions/16387354/template-tuple-calling-a-function-on-each-element
namespace detail
{
  template<int... Is>
  struct seq { };
  template<int N, int... Is>
  struct gen_seq : gen_seq<N - 1, N - 1, Is...> { };
  template<int... Is>
  struct gen_seq<0, Is...> : seq<Is...>{};
  template<typename T, typename F, int... Is>
  void for_each(T&& t, F f, seq<Is...>) {
    auto l = { (f(std::get<Is>(t)), 0)... };
  }
}
template<typename... Ts, typename F>
void for_each_in_tuple(std::tuple<Ts...> const& t, F f) {
  detail::for_each(t, f, detail::gen_seq<sizeof...(Ts)>());
}
// End of for_each_in_tuple implementation
template<typename ...Ts>
struct scoped_lock {
public:
  explicit scoped_lock(Ts&... ts) : mutexes(ts...) {
    lock(ts...);
  }
  scoped_lock(Ts&... ts, std::adopt_lock_t) : mutexes(ts...) {}
  ~scoped_lock() {
    for_each_in_tuple(mutexes, [](auto &m) { m.unlock(); } );
  }
  lock_guard(const lock_guard&) = delete;
  lock_guard& operator=(const lock_guard&) = delete;
private:
  tuple<Ts&...> mutexes;
};
template<typename T>
struct scoped_lock<T> {
public:
  typedef T mutex_type;
  explicit scoped_lock(T& _Mtx) : mtx(_Mtx) {
     mtx.lock();
  }
  scoped_lock(T& _Mtx, std::adopt_lock_t) : mtx(_Mtx) {}
  ~scoped_lock() { 
     mtx.unlock();
  }
  scoped_lock(const scoped_lock&) = delete;
  scoped_lock& operator=(const scoped_lock&) = delete;
private:
  T& mtx;
};
    
template<>
struct scoped_lock<> {
  explicit scoped_lock()  {}
  scoped_lock(std::adopt_lock_t) {}
  ~scoped_lock() {}
  scoped_lock(const scoped_lock&) = delete;
  scoped_lock& operator=(const scoped_lock&) = delete;
};
For the purpose of SG10, we recommend the feature test macro __cpp_lib_scoped_lock