Rafaelo
Rafaelo

Reputation: 153

How to simulate Rust's Mutex<Object> in C++?

Having an object and a mutex as 2 different variables is kinda error prone

MyObject myObject;
std::mutex myObjectMutex;

I tend to forget to lock it sometimes.

On Rust, a shared object is required to be inside a mutex:

std::sync::Mutex<MyObject> mySharedObject

So I have to use like this:

mySharedObject.lock().unwrap().objectMethod()

What would be the less error prone way to simulate something like this in C++ so I don't forget to lock it?

I thought of an std::tuple<std::mutex, MyObject> but it's not very good and I can forget to lock.

Upvotes: 8

Views: 1347

Answers (2)

asmmo
asmmo

Reputation: 7090

I think the following is very near regardless of the lifetime parameter

#include <atomic>

template<class T>
class MutexGuard{
    T& inner;
    std::atomic_flag & locked;
public:
    MutexGuard(T& inner, std::atomic_flag& locked): inner{inner}, locked{locked}{}
    operator T&()&{
        return inner;
    }
    ~MutexGuard(){
        locked.clear();
    }
};
template<class T>
class Mutex{
    std::atomic_flag locked{false};
    T inner;
public:
    Mutex(T&& inner):inner{std::move(inner)}{}
    Mutex(const T&) = delete;
    MutexGuard<T> lock(){
        while(locked.test_and_set());
        return MutexGuard<T>{inner, locked};
    }
};

Demo

Upvotes: 1

Justin
Justin

Reputation: 25277

One way to do it is have your Mutex<T> only allow access to the contained T via a lambda:

template <typename T>
class Mutex {
private:
    T value;
    std::mutex mutex;

public:
    // Fill out some constructors, probably some kind of emplacement constructor too.
    // For simplicity of the example, this should be okay:
    explicit Mutex(T value)
        : value(std::move(value))
    {}

    template <typename F>
    auto locked(F&& fn) const& -> std::invoke_result_t<F&&, T const&> {
        // Lock the mutex while invoking the function.
        // scoped_lock automatically unlocks at the end of the scope
        std::scoped_lock lock(mutex);
        return std::invoke(std::forward<F>(fn), value);
    }

    template <typename F>
    auto locked(F&& fn) & -> std::invoke_result_t<F&&, T&> {
        std::scoped_lock lock(mutex);
        return std::invoke(std::forward<F>(fn), value);
    }

    // Can be worth repeating for const&& and && as well
};

Usage:

mySharedObject.locked([] (MyObject& obj) { return obj.objectMethod(); });

It's still possible to defeat this by stashing a reference to the obj inside a .locked(...) call and using the reference outside of .locked(...), but that would almost require deliberately trying to do the wrong thing.

Also, be aware that it being in a lambda can be quite limiting, as regular control flow no longer works (return doesn't return from the outer scope, continue/break don't work, etc.)

Upvotes: 7

Related Questions