3

Is there a way to ensure that a std::shared_ptr is the only reference to an object in the whole program?

I was under the impression that the following would be sufficient even in a multithreaded environment, but with std::shared_ptr::unique being deprecated I am not so sure (and my concurrent-fu fails me):

std::shared_ptr<Foo> bar(std::shared_ptr<Foo>&& src) {
     std::shared_ptr<Foo> self = std::move(src);
     if(self && self.unique()) {
        // self is *the* only reference
     }

     return std::move(self);
}

(edit)

If I understand correctly (see e.g. Why is std::shared_ptr::unique() deprecated?) the atomic ref-counting guarantees that self is the only live ref to the data inside the if block, but modifications through destructed references are not sequenced-before the call to unique() and may not have completed yet.

Henceforth, there is still a potential data race when modifying *self inside the if block. Is this correct?

max
  • 1,048
  • 10
  • 20
  • 2
    Why not using a `std::unique_ptr` to ensure that? – πάντα ῥεῖ Feb 02 '19 at 10:31
  • The reference may be shared in some other contexts. I just want to ensure some modifications only ever happen with nobody else looking. – max Feb 02 '19 at 10:56
  • 3
    `unique()` suffers from the check being possibly invalidated by the time you use it. You can get a good _guess_, but to ensure it's unique, you have to claim unique ownership and check for success after actually trying. I'm not sure that `shared_ptr` supports that. – chris Feb 02 '19 at 10:58
  • as you can assign any pointer type from a row pointer type, you have no chance for this. But if you run into such kind of trouble, you have a general problem of "ownership" in your code. – Klaus Feb 02 '19 at 10:58
  • For instance, if the reference is the only one I may perform some modifications in-place as an optimization instead of copying. – max Feb 02 '19 at 10:58
  • You can never guarantee that since "someone" can retrieve a raw pointer to the object and do whatever with. – molbdnilo Feb 02 '19 at 10:59
  • 1
    @molbdnilo, You *could* get around the smart pointer trust model with raw pointers, but you're only hurting yourself by doing so. I think it's reasonable to provide ownership guarantees under the assumption that you don't go out of your way to get around them. – chris Feb 02 '19 at 11:00
  • "if the reference is the only one I may perform some modifications in-place as an optimization instead of copying". Thats a totally bad idea. The compiler did a lot of optimizations and you should measure before you try to make any kind of totally obscure assumptions and solutions for problems which do not exist! On the one hand, you do not know how often objects are referenced and on the other hand you try to "optimize" with handcrafted obscurities. First: optimize the design, second: optimize your algos if needed! – Klaus Feb 02 '19 at 11:01
  • 1
    @chris Of course it hurts, but the question was whether it could be made impossible. And the type system of C++ is too primitive for that. – molbdnilo Feb 02 '19 at 11:02
  • 1
    @molbdnilo, I guess I'm reading the question more pragmatically than theoretically. – chris Feb 02 '19 at 11:03
  • @Klaus I'm trying to implement immutable data structures similar to https://www.youtube.com/watch?v=sPhpelUfu8Q and AFACT this is a perfectly legit optimization. – max Feb 02 '19 at 11:05
  • @chris Thanks, that's exactly what I was afraid of (`unique` not being accurate thereby violating my assumptions). What would be a correct alternative? – max Feb 02 '19 at 11:08
  • @molbdnilo Of course this is c++ not rust, but that's exactly the kind of guarantees I'm looking for, except it would be enforced at runtime instead of compile-time. – max Feb 02 '19 at 11:13
  • "I'm trying to implement immutable data structures ... this is a perfectly legit optimization." So you have to start with some design decisions what "ownership" of your immutable copies mean. Having some customers referencing to your immutable's mean that you have to define some kind of tracking your customer/data relationship. Whats about factory/data pool and a pointer implementation which exactly do this job? – Klaus Feb 02 '19 at 11:57
  • @chris If I understand correctly (see e.g. https://stackoverflow.com/q/41142315/1753545) the atomic ref-counting guarantees that `self` is the only live ref to the data in the program inside the `if` block, but modifications through destructed references are not *sequenced-before* the call to `unique()` and may not have completed yet. Henceforth, there is still a data race when modifying `*self`. Is this correct? – max Feb 02 '19 at 12:39
  • 1
    @max, The aspect I was getting at was another part of the program concurrently adding a reference after the `if` says you're the only one, but before you execute code assuming you're the only one. The more common example is a file. For the code `if (file_exists(filename)) parse(read_file(filename));`, the file could be deleted after you check that it exists, but before you read it. The common solution I see in this case is to just try reading it and handle the error you get when it doesn't exist. However, I think applying this to `shared_ptr` is likely not a small change. – chris Feb 02 '19 at 12:50
  • 1
    @chris I see, thanks. My reasoning was that since `self` is moved-from, there would be no way for another thread to add another reference to the object after it is observed `unique`, since the moved-from value would to be empty (I updated the example a bit). – max Feb 02 '19 at 13:32
  • Good point. I'd have to think on it more. – chris Feb 02 '19 at 14:07
  • @max 1) Why do you need the move? 2) Why do you believe the move is safe? – curiousguy Feb 03 '19 at 14:05
  • @chris File existence isn't a big issue; file nature is. Many things can be opened for reading and writings bytes that aren't regular files, in Unix. Opening can have side effects that you don't always want to permit. – curiousguy Feb 03 '19 at 14:06
  • @curiousguy 1) the move empties the rvalue reference argument, which could be shared in the calling context. 2) please define "safe" (memory safe? thread-safe? something else?) – max Feb 06 '19 at 09:44
  • 1
    @max Safe means no UB (at the minimum) – curiousguy Feb 06 '19 at 11:37

1 Answers1

3

This is not sufficient because .unique() (and .use_count()) ignore any weak_ptrs linked to your shared_ptr, which can be .lock()ed concurrently, invalidating your assumptions.

yuri kilochek
  • 12,709
  • 2
  • 32
  • 59