0

I have an instance of class Foo that will be passed a smart pointer to a dependency object. This may be a unique_ptr, if the caller wants to transfer ownership of the object to the Foo instance, or a shared_ptr if the caller wants to share the object with the Foo instance and other things. Perhaps one day it might even accept a weak_ptr so that it can make use of a shared object, for as long as it exists (EDIT: but let's ignore weak_ptr, it clearly complicates things).

My question is, what is the idiomatic way to manage a generic smart pointer in this way?

One idea that comes to mind is to use a shared_ptr for storage, and overloaded functions to load it:

#include <iostream>
#include <memory>

class Bar {};

class Foo {
public:
    void store(std::unique_ptr<Bar> p) { p_ = std::move(p); }
    void store(std::shared_ptr<Bar> p) { p_ = std::move(p); }
    // void store(std::weak_ptr<Bar> p) { p_ = p.lock(); }   // don't worry about weak_ptr
private:
    std::shared_ptr<Bar> p_;  // a shared_ptr can store a unique_ptr, but irreversibly
};

int main() {
    Foo f {};

    // pass ownership of ub to f
    auto ub = std::make_unique<Bar>();
    f.store(std::move(ub));

    // create shared ownership of sb, share with f
    auto sb = std::make_shared<Bar>();
    f.store(sb);
}

I suppose an alternative could be to use a templated class, to determine the storage type at compile time, but in my case I need the Foo instance to potentially accept any of the two pointers at run-time. Also, once the pointer is stored as a shared_ptr, it is not possible to return it to a unique_ptr.

Is there a better approach than two overloads and conversion to a stored shared_ptr?


I'm going to attempt to rephrase my question below, to make it clearer what I'm trying to do. Unfortunately this part is now a "design question" and therefore can't be asked in its own question on SO.

I have a script-driven system that creates properties of a hierarchy of Objects - think 3D objects with associated materials. As the script is parsed, the object hierarchy (tree) is built, and materials are created and associated with these objects. Some materials are solely owned by a single Object, some are shared between multiple objects, and some may be weak references to other materials (if it helps, drop this requirement, I'm more concerned about unique/shared ownership).

The materials are created with a factory function, that currently returns a unique_ptr<Material>. I chose this because Scott Meyers suggests that it is the most versatile type to return from a factory function, because it doesn't require the factory function to know about any final ownership strategy. The caller of the factory function can change this unique_ptr into anything else, like a shared_ptr or even a weak_ptr, depending on how it intends ownership of this Material to be handled.

Then this is passed to an Object, with the caller determining how the Object will manage this material. Perhaps it will be given as sole owner, or perhaps it will be shared between multiple Objects. So an Object needs to be able to accept, and store, either a unique_ptr or shared_ptr. Once it has this pointer, it doesn't actually care what it is, it's really just for Material clean-up once the Object is destroyed.

Comments suggest that this is an unusual pattern - and if so, I'm keen to hear about alternative designs that might accomplish the same thing - which I could imagine could be described as "lifetime management of heap-allocated factory-created dependencies that are injected into another object".

davidA
  • 12,528
  • 9
  • 64
  • 96
  • 2
    I'm note sure I understand. Are you looking for [`std::variant, std::shared_ptr>`](https://en.cppreference.com/w/cpp/utility/variant) ? – Erel Apr 04 '23 at 09:01
  • When you store a pointer in your `Foo` object, do you want to get it back in future? If yes, what type will it have? Also, what should `Foo` do with your pointer? – anatolyg Apr 04 '23 at 09:04
  • 2
    This sounds like a XY problem. Why is the decision made at runtime? – Passer By Apr 04 '23 at 09:05
  • Now that I read your other question, I am almost certain you misunderstood what the pointers are supposed to do and is using them incorrectly. I cannot think of a scenario you should be deciding which pointer to use at runtime. Yet another reason not to take "insight" from ChatGPT. – Passer By Apr 04 '23 at 09:15
  • A `shared_ptr` can't *store* a `unique_ptr`; it can take over ownership of the object it owns. Once an object is shared, it is shared until there are no more owners. – molbdnilo Apr 04 '23 at 09:15
  • @Erel quite possibly - the issue I'm having is writing classes that can have polymorphic dependencies injected but can still be treated like values. I have found something called `polymorphic_value` that might actually be what I'm looking for. – davidA Apr 04 '23 at 09:39
  • Re: XY problem - I have a factory function that returns a `unique_ptr` to some dependency. I then want to pass that dependency into some other object, but the ownership model is determined by the caller. E.g. the dependency may become owned by the accepting object, or maybe it will be shared between many such objects, or maybe it will be cloned and given ownership of an exclusive clone. It needs to be made at runtime because the data structure is decided by config file. E.g. "materials on 3D objects" - some objects share materials with others, some have their own, and those may be copies. – davidA Apr 04 '23 at 09:44
  • @PasserBy in all fairness, my other question was simply about whether factory functions are best returning `unique_ptr`s or values. The AI thing was what I resorted to after my question was closed without explanation. *This* question on the other hand is about how to manage those factory-function created dependencies, now that they are `unique_ptr`s. According to https://stackoverflow.com/a/37885232/, `unique_ptr`s are best as they can be converted to other types - the factory function doesn't need to know. But there are differences in how these dependent objects are subsequently used. – davidA Apr 04 '23 at 09:48
  • I suspect the answer is to just use `shared_ptr` everywhere. – davidA Apr 04 '23 at 09:48
  • _"just use shared_ptr everywhere"_ the other pointers exist for a reason. They have different semantics. If what you have is a shared resource that never requires a weak reference, then obviously use `std::shared_ptr`. But you if you already knew that, why are there even `std::unique_ptr` and `std::weak_ptr` in the first place? – Passer By Apr 04 '23 at 10:27
  • The recommendation from elsewhere (human, not AI) is that `unique_ptr` is the most useful type for a factory function to return, because the client code can convert it into something else if necessary. This makes the client code in charge of the ownership. What I'm getting at is that some consumer of this resource needs to have this ownership transferred to it, by the client code, and it needs to accept it as sole ownership (`unique_ptr`), or shared ownership (`shared_ptr`) but only the client code is in charge of that decision. So the consumer needs to be able to accept either ownership model – davidA Apr 04 '23 at 10:36
  • 1
    *I suspect the answer is to just use `shared_ptr` everywhere.* I think the answer is never use `shared_ptr` at all, unless absolutely necessary. A `shared_ptr` is analogous to a global variable — either is fine if they are immutable, but if they are non-const they make reasoning about the program's correctness much more difficult. – Eljay Apr 04 '23 at 13:26
  • If a runtime decision between unique and shared ownership is essential, then having the storage as `shared_ptr` will impose least overhead. But then you'll need to impose uniqueness criteria through extra members (both data and functions). `weak_ptr` is a missfit in this senario. Because the other 2 are common in that they both take ownership. Unique ownership can be enforced by strictly keeping share count at 1. In the end if the design cannot be modified, the `variant` solution seems the most general; but it is still fishy. – Red.Wave Apr 04 '23 at 17:15
  • I've added some info about the design to the bottom of the question. Let's drop `weak_ptr` - I only included it to be more general, but I can see that it throws a lot of problems into the question. So let's just drop that. – davidA Apr 04 '23 at 20:43

2 Answers2

1

Use shared_por as the main case and when passed a unique_ptr, take the raw pointer and create a shared_ptr from it. Either this is simple or I’m missing something

Something Something
  • 3,999
  • 1
  • 6
  • 21
  • 1
    Yes, I think it is this simple, actually. Originally my question mentioned `weak_ptr` which confused the issue considerably, but I didn't actually need `weak_ptr` so I've removed it from the question title. Thus, I think you're right, it's really just a matter of expecting a `shared_ptr`, and handling the case where the caller passes exclusive ownership via a `unique_ptr`. Thus the caller can decide how the ownership will happen. – davidA Apr 14 '23 at 02:55
0

I'd write a wrapper for this that can deal with any of those pointer types. Note that in your example, the initialisation of the weak pointer is incorrect, as it will grab onto the resource and prevent it from being destroyed. That does not feel like predictable behaviour.

Here's an implementation that can handle all these pointer types, making them just behave as "a pointer".

template <typename T>
class Pointer {
  private:
    std::variant<std::unique_ptr<T>,std::shared_ptr<T>,std::weak_ptr<T>> ptr;
    static std::shared_ptr<T> get(std::weak_ptr<T> const& p) {
      // consider throwing if p.expired()
      return p.lock();
    }
    static auto const& get(auto const& p) {
      return p;
    }
  public:
    Pointer(std::unique_ptr<T> p) : ptr{std::move(p)} {}
    Pointer(std::shared_ptr<T> p) : ptr{std::move(p)} {}
    Pointer(std::weak_ptr<T> p) : ptr{std::move(p)} {}
    Pointer(Pointer const&) = delete;
    Pointer(Pointer&&) = default;
    Pointer& operator=(Pointer const&) = delete;
    Pointer& operator=(Pointer&&) = default;
    ~Pointer() = default;

    auto& operator*() const {
      return std::visit([](auto const& sp) -> T& { return get(sp).operator*();}, ptr);
    }
    auto operator->() const {
      return std::visit([](auto const& sp) { return get(sp).operator->();}, ptr);
    }
};

This is how you would use it:

  Pointer a{std::make_unique<int>(1)};
  assert(*a == 1);
  Pointer b{std::make_shared<int>(2)};
  assert(*b == 2);
  {
    auto const s = std::make_shared<int>(3);
    Pointer c{std::weak_ptr(s)};
    assert(*c == 3);
  }
  Pointer d{std::weak_ptr(std::make_shared<int>(4))};
  assert(*d == 4); // UB, but could be made to throw (see above)

Check out the live demo on compiler explorer.

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • _"throws (as it should)"_ it shouldn't. That's just UB. Your live demo crashed, it didn't throw. – Passer By Apr 04 '23 at 12:25
  • @PasserBy You are correct. I somehow thought `std::weak_ptr::lock()` would throw if expired. But it just creates an empty shared_ptr. I corrected the answer. – bitmask Apr 04 '23 at 12:28
  • But `Pointer` is quite unusable regardless. How can you use it if you can't check if it's valid? – Passer By Apr 04 '23 at 12:29
  • @PasserBy Well, if, as in my edit, you make `get` to throw for weak pointers, that would be one way. But -- you can easily add a member function `expired` that returns `std::weak_ptr::expired()` or `false` depending on the variant's value. – bitmask Apr 04 '23 at 12:32
  • Then `Pointer` will have the clunkiness and overhead of `std::weak_ptr` but only the capabilities of `std::unique_ptr`. Does that not strike you as very flawed design? The point I'm getting at is the entire premise is wrong, OP should redesign. – Passer By Apr 04 '23 at 12:37
  • @PasserBy The restrictions are a consequence of the requirements of abstracting all three types. – bitmask Apr 04 '23 at 12:46
  • "OP should redesign" - well, this is fun. I started by asking about a suitable design and my question was closed. You all probably have colleagues you can discuss C++ with every day. I do not. I don't know a single other C++ programmer in my entire country. I have to figure most of this stuff out for myself, or ask SO. I just want to learn how to write modern C++ properly, and @PasserBy I've been finding you increasingly antagonistic. – davidA Apr 04 '23 at 20:18
  • Since design questions get closed on SO, where else can I ask design questions? Is there another site more accommodating to this kind of query? – davidA Apr 04 '23 at 20:32
  • I've added some design info to my question, as background. Hopefully it won't get my question closed. – davidA Apr 04 '23 at 20:47
  • @davidA It's unfortunate you find this antagonistic. If I am truly antagonistic I'd just downvote and leave. I pointed out that you are very likely misusing the various pointers and we see the ill consequences of that in the answer. What you will do with that is up to you. – Passer By Apr 04 '23 at 20:47
  • @PasserBy sorry, I just feel like I go round and round on this site, unable to ask my exact question (closed as "opinon"), so having to create contrived snippets that hint at what I'm trying to do. Ultimately I just want to ask experienced C++ programmers how they'd approach a particular problem, but this site doesn't seem like the right place to do that. – davidA Apr 04 '23 at 20:49
  • @davidA: Maybe you could raise your situation on meta. Perhaps (!) you will get information on *how* to formulate the problem in a way that works on SO. I am also sometimes frustrated that questions get closed too hastily (especially seconds before I get to post an already written answer). However, in theory the close mechanism is there for good reasons, even if it can sometimes be frustrating in practice. – bitmask Apr 05 '23 at 07:29
  • @davidA: Regarding your update in the original post. If I understand you correctly I would suggest to simply convert the unique_ptr into a shared_ptr. If you are worried about overhead create the shared_ptr from the factory for the specific types are are *sometimes* shared. You don't gain much from having some objects stored in an unique_ptr if you have to make a case distinction every time. – bitmask Apr 05 '23 at 07:34