Chris Morgan
Chris Morgan

Reputation: 1339

Returning an RAII container class that holds a std::mutex lock

I'd like to wrap all usages of a class instance with a mutex. Today I have

std::map<int, std::shared_ptr<MyClass>> classes;

and functions to find and return instances, like:

std::shared_ptr<MyClass> GetClass(int i);

I'd like to ensure that GetClass() can only retrieve an instance if someone else hasn't already retrieved it, with some RAII mechanism. Usage would be like:

void CallingFunction()
{
    auto c = GetClass(i); // mutex for class id 'i' is acquired here

    // some calls to class
    c.SomeFunction();
} // mutex is released here when 'c' goes out of scope

With the mutex acquired by CallingFunction() other threads that wanted to access the same class instance would block on their calls to GetClass().

I've been looking at a few ways of doing it, such as with a wrapper class like:

class ClassContainer
{
    public:
        std::shared_ptr<Class> c;
        std::mutex m;
};

Where I'd modify GetClass() to be:

ClassContainer GetClass(int i);

But I'm having trouble figuring out both where the std::mutex should be kept, I tried initially storing it in the map before moving to using a container class like:

std::map<int, std::pair<std::mutex, std::shared_ptr<MyClass<>>> classes;

but that wasn't working well, now with the ClassContainer how to have ClassContainer lock the std::mutex like std::lock_guard<> when the caller acquires one via a call to GetClass().

Upvotes: 1

Views: 1047

Answers (3)

Slava
Slava

Reputation: 44278

I've been looking at a few ways of doing it, such as with a wrapper class like:

Yes this is proper way to do it and you are close, but you cannot keep mutex itself in this class, only locker. And std::unique_lock is a proper type for that as it has necessary move ctor etc. I would make fields private though and create necessary accessors:

class ClassContainer
{
    std::shared_ptr<Class> c;
    std::uniqe_lock<mutex> lock;
public:
    ClassContainer( std::pair<std::mutex,std::shared_ptr<Class>> &p ) :
        c( p.second ),
        lock( p.first ) 
    {
    }
    Class * operator->()const { return c.get(); }
    Class & operator*() const { return *c; }
};

then usage is simple:

void CallingFunction()
{
    auto c = GetClass(i); // mutex for class id 'i' is acquired here

    // some calls to class
    c->SomeFunction();
    // or even
    GetClass(i)->SomeFunction();
} 

Upvotes: 2

Jarod42
Jarod42

Reputation: 218098

It is Class which should hold the mutex, something like:

class Class
{
public:

    // Your methods...

    std::mutex& GetMutex() { return m; }
private:
    std::mutex m;
};

class ClassContainer
{
public:
    ClassContainer(std::shared_ptr<Class> c) :
         c(std::move(c)),
         l(this->c->GetMutex())
    {}

    ClassContainer(const ClassContainer&) = delete;
    ClassContainer(ClassContainer&&) = delete;
    ClassContainer& operator =(const ClassContainer&) = default;
    ClassContainer& operator =(ClassContainer&&) = default;

    // For transparent pointer like access to Class.
    decltype(auto) operator -> () const { return c; }
    decltype(auto) operator -> () { return c; }

    const Class& operator*() const { return *c; }
    Class& operator*() { return *c; }

private:
    std::shared_ptr<Class> c;
    std::lock_guard<std::mutex> l;
};

ClassContainer GetClass(int i)
{
    auto c = std::make_shared<Class>();
    return {c}; // syntax which avoids copy/move contructor.
}

and finally usage:

auto&& cc = GetClass(42); // `auto&&` or `const&` pre-C++17, simple auto possible in C++17

cc->ClassMethod();

Simplified demo.

Upvotes: 1

SergeyA
SergeyA

Reputation: 62603

Accidentally, I did something extremely similar recently (only I returned references to objects instead of shared_ptr. The code worked like following:

struct locked_queue {
    locked_queue(locked_queue&& ) = default;
    mutable std::unique_lock<decltype(queue::mutex)> lock;
    const queue::q_impl_t& queue; // std::deque
};

And here is how it would be used:

locked_queue ClassX::get_queue(...) {
    return {std::unique_lock<decltype(mutex)>{mutex}, queue_impl};
}

Upvotes: 0

Related Questions