0

I have a situation where I have a piece of hardware and it really only makes sense for a single connection to be open on that hardware at the same time, so I really only want to refer to one object no matter where it's passed. So it may look something like:

class Hardware {
public:
   void Open(); //this will create and launch a connection monitor in the background
   void Close();
   std::string SendPing();
   std::string AskForSomeOtherData();
   bool isConnected();
   bool setConnected(bool connState);
private:
   bool connected_;
   int hndl_;
}

I then have another class monitoring the connection in the background so my UI can be notified if a connection is lost.

class ConnectionMonitor{
    ConnectionMonitor(Hardware& hw);
    void Run(); //launches a background thread to send "pings" to the device to make sure it's there. Will update Hardware.connected_
    Hardware hw_; //or Hardware* hw_, or shared_ptr<Hardware> hw_, or Hardware& hw_;
}

When the connection monitor notices a connection is lost, it needs to update the connected boolean of the hardware object passed in directly, because my other threads using the object will also want to know it's been disconnected. But I don't know if updating the private data of the object indicates ownership of the object itself?

I don't believe so, since the lifetime of the object shouldn't be effected. There are all these different ways to pass an object shared between two classes, but I don't know what the problem domain calls for when I need to store it in class private data. Requiring a shared pointer to be passed in is one option. But I am not really indicating any ownership of the object being taken I don't think? A reference seems to be more fitting possibly, but then I end up with needing Hardware& hw; in my ConnectionMonitor private data so I keep a reference to the Hardware object. Then I read stuff that says "Reference in private data bad."

Also, if the caller is storing the hardware object as a shared_ptr, I believe I would have to do something like the following to pass it into my ConnectionMonitor:

ConnectionMonitor(*hardware);

but is this ok for a shared pointer, to dereference it, pass the object to a constructor, and then have the consuming class store another pointer to the same object that the shared pointer owns?

ConnectionMonitor(Hardware& hw){
   Hardware* = hw; //not 100% sure if this syntax is correct, still learning. Most importantly here I'd be taking the hw object being referenced and pointing at it internally, rather than creating a copy
}

It seems now I am now creating a pointer to an object that the shared pointer already owns.

There are a lot of questions embedded above in what amounts to be a brain dump on my thought process, so I will try to summarize. When two threads need access to the same resource, and that resource needs to be updated from either thread, is this a case for a shared pointer? If one of the classes is not taking ownership, what is the best way to store that shared object in private data?

gfree
  • 479
  • 7
  • 16
  • 5
    The non-owning counterpart to `std::shared_ptr` is `std::weak_ptr` – UnholySheep Aug 08 '22 at 19:15
  • Side note: `connected_` should be given some concurrency defense [`std::atomic`](https://en.cppreference.com/w/cpp/atomic/atomic) or [`std::atomic_flag`](https://en.cppreference.com/w/cpp/atomic/atomic_flag) seem appropriate, but might not be enough depending on how much defense the whole transaction might require. Sucks to be disconnected half way through a long operation that assumes a connection. – user4581301 Aug 08 '22 at 19:18
  • The weak pointer comment is useful, this makes sense. My follow up would be, however, doesn't this dictate from a consuming class how the owning class must manage the pointer they have? Maybe that's ok, but it is a point I want to bring up. I guess I could flip that on its head and say the non-owner is dictating its interface for passing data, and the owner must adhere to that. – gfree Aug 08 '22 at 19:47
  • Ref, shared_ptr or singleton. Do you want to subscribe to the destruction of `Hardware` instance from your `ConnectionMonitor`, or do you expect the former to always outlive the latter? – lorro Aug 08 '22 at 19:59
  • @lorro - I guess there are two ways to solve this problem. a) the hardware object is created by a caller. Then the caller passes the hardware object to the connection monitor. Or b) the hardware object itself creates the connection monitor, and the connection monitor's lifetime is explicitly tied to the lifetime of the hardware. A *this* pointer would be passed to the monitor. I am leaning towards b) since the outside world doesn't need to know about the connection monitor, it's more of an implementation detail of the hardware. – gfree Aug 08 '22 at 20:15
  • Have you thought of a [singleton](https://stackoverflow.com/questions/1008019/c-singleton-design-pattern/1008289#1008289)? – Martin York Aug 08 '22 at 20:21
  • 1
    @MartinYork - I have but am not prepared to implement that constraint, because it's possible in the future a second hardware device would be plugged in and I would need two instances. – gfree Aug 08 '22 at 20:23
  • @gfree: Its easy to change the "Singleton" pattern into a named object pattern. Just change the `getInstance()` take a name and change the static object into a map (or other appropriate storage). – Martin York Aug 08 '22 at 20:27
  • @gfree Disclaimer, I'm very much against the singleton pattern (in real-life informatics, almost nothing is a singleton), but having a simple static shared instance (or pointer or `optional`) makes sense here. Also, I'd still verify if the common object exists before accessing it. That's one way to do it; another way is to link the objects together in e.g. `main()`. Both work well and can be extended to multiple instances. – lorro Aug 08 '22 at 20:29

1 Answers1

1

but is this ok for a shared pointer, to dereference it, pass the object to a constructor, and then have the consuming class store another pointer to the same object that the shared pointer owns?

Yes raw pointers are fine when the are non-owning. You can use std::shared_ptr::get to retrieve the stored raw pointer. However, when you store the raw pointer you need to consider the lifetime of the object. For example this is fine:

#include <memory>
#include <iostream>

struct bar {
    std::shared_ptr<int> owning_ptr;
    bar() : owning_ptr(std::make_shared<int>(42)) {}
    ~bar() { std::cout << "bar destructor\n"; }
};
struct foo {
    int* non_owning_ptr;
    ~foo() { std::cout << "foo destructor\n"; }
};


struct foobar {
    bar b;
    foo f;
    foobar() : b(),f{b.owning_ptr.get()} {}
};

int main() {
    foobar fb;
}

b gets initialized first and f can use a pointer to the int managed by owning_ptr. Because f gets destroyed first, there is no danger of f using non_owning_ptr after owning_ptr already deleted the int.

However, only in certain circumstances you can be certain that the raw pointer can only be used as long as the object managed by a smart pointer is alive. As mentioned in a comment, the non-owning counter part to std::shared_ptr is std::weak_ptr. It does not increment the reference count, hence does not prevent the managed object to be deleted when all std::shared_ptrs to the object are gone.

#include <memory>
#include <iostream>

struct moo {
    std::weak_ptr<int> weak;
    void do_something() {
        std::shared_ptr<int> ptr = weak.lock();
        if (ptr) { std::cout << "the int is still alive: " << *ptr << "\n"; }
        else { std::cout << "the int is already gone\n"; }
    }
};


int main() {
    moo m;
    {
        std::shared_ptr<int> soon_gone = std::make_shared<int>(42);
        m.weak = soon_gone;
        m.do_something();
    }
    m.do_something();
}

Output is:

the int is still alive: 42
the int is already gone

As already mentioned the std::weak_ptr does not keep the object alive. Though, it has a lock method that locks the object from being destroyed as long as the returned std::shared_ptr is alive. When lock is called on the weak_ptr when the object is already gone, also the returned shared pointer is empty.


When two threads need access to the same resource, and that resource needs to be updated from either thread, is this a case for a shared pointer?

Yes and no. The ref counting of std::shared_ptr is thread safe. Though using a shared pointer does not automagically make the pointee thread safe. You need some synchronization mechanism to avoid data races.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • How about std::atomic> from c++20? – Mikel F Aug 08 '22 at 21:45
  • @MikelF I am not aware of something new about `std::atomic>` in C++20, though I am not too familiar with all C++20 features – 463035818_is_not_an_ai Aug 09 '22 at 08:23
  • Oh well, I was hoping you might have a better handle on it. I haven't been as active tracking the standard as I was a few years ago, but I do know they have added functionality in the space (as far as cppreference.com is concerned). – Mikel F Aug 09 '22 at 20:52
  • A shared_ptr is generally not shared between threads but the mechanism by which you share a resource through copying the shared_ptr. A atomic shared_ptr would be more like `T**`, you have to pass it by reference to make any sense. You might even want a `std::shared_ptr>>` unless you guarantee the owner of the pointer will outlive all threads that get a reference. – Goswin von Brederlow Aug 11 '22 at 18:10