1

I wanted to ask if there is a smart pointer that could take in any class in its template and then any operations done with such pointer would result in a thread-safe operation.

Basically an idea would be that such pointer would automatically hold an internal lock during a scope and release it when the pointer goes out of scope.

Use case would be for example to pull such pointer from a static, pre-allocated array into some scope and perform thread-safe operations inside that scope on the object itself.

I tried to find a C++ library/feature that could perhaps allow for some thread-safe mutation on objects by wrapping it into a single smart pointer object.

  • no. smart pointers allow you to get a raw pointer to the contained object and by this bypass all locking it could provide. – 463035818_is_not_an_ai Mar 28 '22 at 13:15
  • See [this post](https://stackoverflow.com/questions/692438/is-boost-shared-ptr-xxx-thread-safe) – Inigo Selwood Mar 28 '22 at 13:16
  • Hrm, well - I do know that in a shared_ptr, only the access to the control block (basically the whole reference counting) is thread-safe, which assures that such memory will only be deleted once and safely... But I want more than that! – Mike Tom Hommel Mar 28 '22 at 13:18
  • I think it would require you to store a mutex object inside each such "smart pointer". And in that case it would be hell to program, because if you happen to spawn two pointers in the same scope you get a silent deadlock – Alexey S. Larionov Mar 28 '22 at 13:18
  • @AlexeyLarionov Thanks for your comment, this is of course a big problem that I haven't thought of, as I am admittedly new to the whole RAII concept of C++ (I am originating from C) – Mike Tom Hommel Mar 28 '22 at 13:19
  • @AlexeyLarionov Not as long you have only one object which is controlled by a set of smart pointers. But that is the same as with every other locked data protection, fully independent if your smart pointer itself controls the access order. – Klaus Mar 28 '22 at 13:20
  • @AlexeyLarionov Welp, same story with mutexes. Once using such pointers, I would try to use them as wisely as possible - by simply minimalizing their use to not have a whole mutex lock mess. But I had assumed that the threat of a deadlock is just as real as when using normal mutexes, ugh. – Mike Tom Hommel Mar 28 '22 at 13:27
  • 1
    I think the best you could achieve is to avoid race conditions (somewhat inefficiently). Multithreading is too complicated because you often want to lock an object beyond a single operation. – Galik Mar 28 '22 at 13:27
  • @Taekahn Nice try – I fear in most cases this would already fail for the type not being trivially copiable (std::vector, for instance, isn't). And how would that protect multiple subsequent writes from within a member function? Locking and unlocking every single access can lead to inconsistent data or other errors. – Aconcagua Mar 28 '22 at 13:33
  • When I was playing with this concept I developed this approach. It is not perfect, but it is fairly safe and relatively efficient. https://stackoverflow.com/a/50950667/3807729 – Galik Mar 28 '22 at 13:39
  • You can't force thread safety on code that wasn't designed to be thread-safe. Protecting the data in an object can prevent data races, but that's only part of making an application thread-safe. For example, `for (int i = 0; i < data.size(); ++i) std::cout << data[i] << '\n';` won't have any data races if `data` properly protects its internals, but it is not thread-safe. Another thread could remove an element from `data` between `i < data.size()` and `data[i]`, and `data[i]` could go out of bounds. – Pete Becker Mar 28 '22 at 14:42

3 Answers3

4

if there is a smart pointer that could take in any class in its template and then any operations done with such pointer would result in a thread-safe operation.

No, there is no such smart pointer in the C++ standard.

eerorika
  • 232,697
  • 12
  • 197
  • 326
2

I don't think that's possible in the "usual" smart pointer sense, because when doing ptr->something() or (*ptr).something(), the operator-> and operator* methods are called, they return the pointer/reference and then something is invoked, so you don't have any way to know when to unlock the mutex after the operation has been done. This can be worked around through proxy objects, but that's another can of worms, especially when mixed with usage of auto.

Moreover, on a higher level this is rarely a kind of thread-safety guarantee one actually needs. In a codebase of ours someone once wrote a wrapper for std::map with a mutex protecting some common mutation operations; this was eminently useless for several reasons. The most obvious was that operator[] returns a reference anyway (so, you get a reference that may be instantly invalidated by someone else calling e.g. erase()); but most importantly, people did stuff like if (!map.count(key)) { map[key].do_something(); }, ignoring the fact that the result of count became stale immediately.

The takeaway here is that generally mutex-wrapping single operations on an objects doesn't gain you much: to actually work safely in a sane manner usually you need to take a mutex for a longer period, to ensure your code has a consistent snapshot of the protected object state.


A possibility to attack both these problems is to turn the whole thing to a different angle: you may wrap your object in an "escrow" object that forces you to take the mutex to access the data, but also think in terms of "doing all the operations where you need it" in a single "mutex-take". A sketch may be something like:

template<typename T>
class MutexedPtr {
    std::mutex mtx;
    std::unique_ptr<T> ptr;
public:
    MutexedPtr(std::unique_ptr<T> ptr) : ptr(std::move(ptr)) {}
    

    template<typename FnT>
    void access(FnT fn) {
        std::lock_guard<std::mutex> lk(mtx);
        fn(*ptr);
    }
};

The usage should be something like:

MutexedPtr<Something> ptr = ...;
...
ptr.access([&](Something &obj) {
    // do your stuff with obj while the mutex is taken
});

whether this is something that could be useful to your use case is up to you.

Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
  • You're right, that's why I was asking for the scope to be guarded by some mutex that would have been embedded into such imaginary type of smart pointer to then just unlock that mutex when the pointer itself goes out of scope (just like a shared_ptr, except not only for reference counting but to protect the whole object). Sadly it seems to be a bad idea?! – Mike Tom Hommel Mar 28 '22 at 13:32
  • @MikeTomHommel: the most similar thing that comes to mind is what Ayxan Haqverdili is proposing in their answer, but it seems to me more trouble than it's worth. – Matteo Italia Mar 28 '22 at 13:34
  • How is your MutexedPtr any better than my design? I can still store the pointer I got, right? `Something* ref; ptr.access([&](Something& obj) { ref = &obj; });`. It's just a way more cumbersome way of doing the exact same thing. – Aykhan Hagverdili Mar 30 '22 at 18:45
1

I wanted to ask if there is a smart pointer that could take in any class in its template and then any operations done with such pointer would result in a thread-safe operation.

Yes, that's possible. Here's a simple implementation:

#include <thread>
#include <mutex>
#include <cstdio>

template <class T>
struct SyncronizedPtrImpl {
private:
    std::scoped_lock<std::mutex> lock;
    T* t;

public:
    SyncronizedPtrImpl(std::mutex& mutex, T* t) : lock(mutex), t(t) {}

    T* operator->() const { return t; }
};


template <class T>
struct SyncronizedPtr {
private:
    std::mutex mutex;
    T* p;
public:

    SyncronizedPtrImpl<T> operator->() {
        return SyncronizedPtrImpl<T>{mutex, p};
    }

    SyncronizedPtr(T* p) : p(p) {}
    ~SyncronizedPtr() { delete p; }
};

int main() {
    struct Foo {
        int val = 0;
    };

    SyncronizedPtr ptr(new Foo);

    std::thread t1([&]{
        for (int i = 0; i != 10; ++i) ++ptr->val;
    });

    std::thread t2([&]{
        for (int i = 0; i != 10; ++i) --ptr->val;
    });

    t1.join();
    t2.join();

    return ptr->val == 0;
}
Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • Storing locks and mutexes as members is quite problematic since they're neither copyable (which is fine) nor movable (significant limitation). – eerorika Mar 28 '22 at 13:31
  • Giving back the internal raw pointer is a fat open door to break all and everything :-) I believe that will never be production code... – Klaus Mar 28 '22 at 13:32
  • @eerorika well, that wasn't a design goal – Aykhan Hagverdili Mar 28 '22 at 13:32
  • 2
    @Klaus any interface can be abused with enough motivation. This interface is relatively easy to use and hard to abuse. – Aykhan Hagverdili Mar 28 '22 at 13:33
  • This interface is fully open for abuse. And no, interfaces can be designed to be not breakable, especially not such easy as here... – Klaus Mar 28 '22 at 13:34
  • @Klaus show me how this is open for abuse, short of storing the pointer you got. Also, most of the nice interfaces are one memcpy away from breaking. – Aykhan Hagverdili Mar 28 '22 at 13:36
  • `std::string* rawptr = (ptr.operator->()).operator->();` – Klaus Mar 28 '22 at 13:41
  • 3
    Sure, and `delete unique_ptr.get();` will break `std::unique_ptr`. Reasonable people write reasonable code. – Aykhan Hagverdili Mar 28 '22 at 13:44
  • Good point :-) ! – Klaus Mar 28 '22 at 13:46
  • I think that would yet need an `operator*` returning a `T&` – how to otherwise forward the wrapped `std::string` to a function accepting e.g. a const reference? – Aconcagua Mar 28 '22 at 13:53
  • @Aconcagua you don't, because that function is expecting an *unsynchronized* string, while you have a synchronized string. Have that function either take `SyncronizedPtr`, or send it a copy, or use something else. – Aykhan Hagverdili Mar 28 '22 at 13:57
  • You'll need a `std::recursive_mutex`! What about `std::sort(ptr->begin(), ptr->end())`? – Aconcagua Mar 28 '22 at 13:57
  • What's the issue about? One doesn't want the string to get modified while that function is yet processing, and that's all. If another thread wants to use it, it needs to wait until the function is finished. `someFunction(*ptr)` would be totally fine, and the temporary `SyncronizedPtrImpl` returned that way would live until the entire expression has been completed anyway. Can we get there without having to write `**ptr`, by the way? Possibly another wrapper type offering an implicit cast operator? – Aconcagua Mar 28 '22 at 14:03
  • *'Yes that's possible'* – you might perhaps want to change to *'No there isn't – but it is possible to write one on your own'* as question asked for the standard library... – Aconcagua Mar 28 '22 at 14:11
  • @Aconcagua as I understand, the question wants to lock per every member access. Also, you don't know if T is const qualified or not, so it's not necessarily a pointer to non-const. And there is nothing wrong with this design, std:: unique_ptr does exactly the same. There is a difference between a const pointer and a pointer to const, and this design expresses that. – Aykhan Hagverdili Mar 28 '22 at 14:41
  • Ah, indeed, you are right – missed that the pointers constness doesn't influence `T`'s constness ;) Too much used to *other* types indeed needing both ;) – Aconcagua Mar 28 '22 at 15:20
  • Just noticing: While there doesn't speek anything against `sort`-ing the string we fail on `std::find`, as it returns an iterator that *still* should be within scope of the lock, but isn't, if called as in above example for `std::sort`. The idea is great, but I start fearing that the use cases are just too limited for the approach being fully valuable :( – Aconcagua Mar 28 '22 at 15:31
  • 1
    Interesting solution to the problem. As far as being "abused" goes... "C++ tries to guard against Murphy, not Machiavelli." — Damian Conway – Eljay Mar 28 '22 at 16:12