During a recent code review, I came across something similar to the following two lines of code.

std::string string; // protected by string_mutex
std::mutex string_mutex;

There is, in principle, nothing wrong with these lines and if I came across those before experimenting with multi-threading in Rust I would probably have never raised an eyebrow. However, after seeing how Rust deals with mutexes it bothered me, that a comment (or naming guidelines) is needed to express that access to the string variable should be protected by the mutex string_mutex. Also, it is possible to use string without locking the mutex by accident.

C++ can do better, so I wrote a tiny wrapper library around std::mutex and std::shared_mutex: cpp-sync. The concept is simple: cpp-sync defines the class sync::mutex<T> that synchronizes the access to data of type T. The constructor takes the initial value for the T, e.g.,:

sync::mutex<std::string> string("")

After construction, to gain access to the value of string you need to call sync::mutex<T>::lock(). This returns a lock guard (a thin wrapper around std::unique_lock<Mutex>) which provides access to the std::string similarly to smart pointers or std::optional via overloading the * and -> operators:

auto value = string.lock();
value->length();
std::string copy = *value;

Like std::unique_lock the mutex will get automatically unlocked when value gets out of scope. This way you can never accidentally access the string without locking the corresponding mutex and the declaration makes the code a lot more expressive in my opinion. It can also make the code using the variable look cleaner as

std::unique_lock<std::mutex>(string_mutex);
string.append("foo");

becomes a single line:

string.lock()->append("foo");

Also, did you notice that I forget to give the unique_lock a name? This is a very subtle and easy-to-overlook bug that causes the destructor of the lock to run immediately, so the access to string is not actually synchronized.

When you have a single mutex that is responsible for syncing multiple variables you need to create a small struct (or tuple) for those variables. E.g.:

struct Foo {
    std::string a_string;
    std::vector<int> some_numbers;
};
sync::mutex<Foo> synced_foo({
    .a_string = "foo", 
    .some_numbers = { 0, 5 },
});

In addition, to sync::mutex<T>, the library also provides sync::read_write_lock<T> which provides essentially the same convenience for a std::shared_mutex. For naming, I oriented myself more on Rust’s RwLock as I, personally, find it more expressive. However, I could also see that it is more confusing to mix terminologies here. Similar to sync::mutex<T>::lock() it provides two member functions: sync::read_write_lock<T>::read() and sync::read_write_lock<T>::write() which return a lock guard for reading and writing respectively. There can be multiple readers simultaneously, but there can be only ever one writer. The lock guard returned from read() provides access to a const T& while the lock guard returned from write provides access to a non-const T&. Here I make the assumption that it is fine to call all const methods on a type from multiple threads. This is true for all types provided by the standard library, but you need to be careful with third-party types or your own types, e.g., when using mutable members.

If you are interested, take a look at the Github project, and feel free to give your opinion on naming or talk about additional features you want to see.