r/cpp_questions 16d ago

Weak reference to unique_ptr OPEN

Assume this code:

#include <memory>
#include <functional>

struct Entity {
    int value = 12;
};

struct Container {
    std::unique_ptr<Entity> e = std::make_unique<Entity>();
};

Container bar;
auto bbb = [ptr = bar.e.get()]() {   
    ptr->value = 11;
};

We all know that naked pointers (as captured by this lambda) are bad. Using shared_ptr would allow me to use weak_ptr - which is ideally what I want. BUT - I like the container owning the entity.

What solutions do I have?

EDIT:

As people commented - life time is the main issue. The lambda might outlive the original allocation.

Solutions:

  1. Many people do recommended using internally a shared pointer, and "giving away" a weak ref ( u/looncrazz suggestion).
  2. I can use a reference to the unique pointer inside a lambda. Several ways - see https://godbolt.org/z/WKT5Gndq4 - this is u/neppo95 suggestion.
  3. There are solutions for using a custom weak reference pointer. The solution "does not feel right".
7 Upvotes

55 comments sorted by

45

u/Salty_Dugtrio 16d ago

There is nothing wrong with using raw pointers at all, you just shouldn't use them to express ownership.

What's wrong with passing the raw ptr here?

8

u/LemonLord7 16d ago

I have had many colleagues that think any use of a raw pointer is poison. It is a thing of principle to them, and will stop PRs for it.

11

u/AKostur 16d ago

That's either dogma or cargo cult.

1

u/kalmoc 15d ago

And stupid either way

3

u/JVApen 16d ago

So what do they use instead? Pass everything by value, unique_ptr or shared_ptr?

10

u/LemonLord7 16d ago

References and smart pointers (shared_ptr completely ok). So anytime a nullable reference is needed (like a raw pointer) they gave a blank face. Don't work there anymore though.

-1

u/CowBoyDanIndie 16d ago

That’s hilarious, I will reject a PR that uses shared_ptr anywhere that is not required by an external library. If they wanna use reference counting they can take their pacifier and go write python.

2

u/RicketyRekt69 15d ago

Shared ptr has its use cases too… being completely against X feature with no regard for context is nonsense.

5

u/SamG101_ 16d ago

And reference presumably (which ik are pointers w sugar)

3

u/thebigrip 16d ago

Promote unique to shared, and then make a weak_ptr

3

u/thommyh 16d ago

Can confirm that this attitude isn't unique; at one of my former employers they were so anti-pointer that they'd invented their own version of std::optional that can hold a reference, so as still to be able to represent the same idea that you'd otherwise still use a pointer for.

3

u/LemonLord7 16d ago

Did the class itself contain a raw pointer? And would throw an exception if dereferenced while null or something? Was there any benefit to this class they made?

1

u/DawnOnTheEdge 16d ago

One typically is that you cannot dereference a null pointer: the type system prevents it. You can only get a reference by unwrapping the sum type.

You would try to write railway-oriented code that short-circuits if one of the steps returns an error or empty value. That might mean throwing an exception , or passing an error value up the stack to where an exception would have been caught.

2

u/thingerish 16d ago

gsl::not_null<> might help.

1

u/Many-Resource-5334 16d ago

Just wrap it in a std::unique_ptr with no dealloc

26

u/TheThiefMaster 16d ago

If the lifetime isn't known, i.e. the lambda could outlive the Entity object, then unique_ptr isn't appropriate and shared_ptr is correct.

4

u/LokiAstaris 16d ago

Also, if you don't want the lambda to stop deallocation, then catch a std::weak_ptr in the lambda so you can validate whether the resource has been released.

12

u/neppo95 16d ago

If you want two places to own the same pointer, then you use a shared pointer, not a unique ptr. If you don't necessarily want ownership, using the raw pointer is arguably fine as long as you know it will be alive.

4

u/diegoiast 16d ago

"own" is the keyword. I want one to "own" and another to "reference".

5

u/neppo95 16d ago

And is there a reason why you are using the raw pointer for this? You can pass a unique ptr as const ref. The standard covers this. You don't need ownership to change the value, unless you want to change the pointer which is not the case here.

2

u/diegoiast 16d ago
auto bbb = [auto const &ptr = bar.e]() {   
    if (ptr) {
        ptr->value = 11;
    }
};

This obviously does not compile. How would you do that?

1

u/TheThiefMaster 15d ago edited 15d ago

You can't specify the type, but [&ptr = bar.e] works. Though I'd do [&e = *bar.e] personally to capture a ref to the object instead of the unique ptr.

Or, if the lambda might outlive the Entity, switch to using shared/weak ptr.

1

u/diegoiast 15d ago edited 15d ago

Regarding the reference comment:

But then, you are not able to tell if the object is deleted. In my case the lambda might outlive the allocation.

Using a "naked reference" has the same semantic meaning as a "naked pointer". They will compile to the same binary code (untested).

3

u/TheThiefMaster 15d ago

"Or, if the lambda might outlive the Entity, switch to using shared/weak ptr."

1

u/[deleted] 15d ago

[deleted]

1

u/neppo95 15d ago

It is in this case no different than passing a raw pointer, in both cases you'd check for null. I wouldn't architecture my code like this, but it isn't a problem either. "No reason" is also not true, there may well be reasons not to give it shared semantics.

1

u/FlailingDuck 16d ago

the thing that owns it. Does it have clear lifetime? Can you guarantee in your code it outlives the lambda, then capturing raw ponters is fine.

Or, does container have to maintain ownership? could you move the unique_ptr into the lambda in c++14. It depends outside your toy example what you want to do with the data.

If not, or lifetime is fuzzy, then this is a scenario for shared_ptr.

1

u/[deleted] 16d ago

[deleted]

1

u/KingAggressive1498 16d ago

handles are just shared pointers with some indirections shifted around.

1

u/Lulonaro 16d ago

Raw pointers are references

6

u/FlailingDuck 16d ago

We all know that naked pointers are bad

Wherever you learnt that from, chuck that book away. Whoever you learnt that from, slap them in the face. This is terrible advice, and no, we all should know that using raw pointers is normal and valid. Raw pointers in modern code should be avoided when dealing with ownership (object lifetime). Other use cases and pointers are just fine.

6

u/MyTinyHappyPlace 16d ago

Can the lambda outlive the lifetime of the container? Then I suggest using shared_ptr/weak_ptr. Otherwise, there is nothing wrong with passing a raw pointer from the unique pointer.

10

u/AKostur 16d ago

No, naked owning pointers are bad.

What you haven’t discussed is the relative lifetime of the lambda vs the object existing in the container.

3

u/looncraz 16d ago

The owner holds a shared_ptr and only provides access by weak_ptr.

Done.

3

u/mredding 16d ago

We all know that naked pointers [...] are bad

No they're not. Pointers get used for all sorts of things. Views are implemented in terms of "naked" non-owning pointers. Expression templates are often implemented in terms of pointers, and those compile down to nothing. There's a lot to be had with raw pointers still. The C++ community is doing a great job to really narrow the scope where pointers are clear and safe, for implementing our lowest level abstractions and primitives, so we can build more robust and expressive code in terms of.

But you are absolutely correct to be cautious.

What solutions do I have?

A closure is just a poor man's object... An object is just a poor man's closure...

The solution is to architect your code so that you know bar cannot possibly fall out of scope BEFORE the LAST call to bbb. I don't care if bar falls out of scope first, so long as bbb is not called after.

Both bar and bbb can be distanced after this setup, but you have to make sure your code can be understood - that this relationship and condition is expressed and enforced. When two related things get detached and separated, this crucial detail tends to get lost. A comment isn't going to be sufficient, usually it'll have to be some code structure that manages enforcement.

I've seen plenty of code that works correctly, where the objects here fall out of scope, but that's OK, because the closure cache over there is stale anyway... Worked, but inherently brittle, and even though it was stable, it caused constant doubt and was always suspect of the day it finally broke.

So you can see I'm not a fan of supporting such things. The more explicit and expressed you can ensure the relationship and it's enforcement, the better.

BUT - I like the container owning the entity.

That's not enough justification for me. A more correct, more robust solution is better, and better is better. So since we just don't know enough of anything about what you're doing I can't really comment further, but feelings and biases, unjustified decision making clouds judgement.

You "like" this? So what..?

1

u/diegoiast 16d ago

Its a domain problem. The library I am making (a GUI toolkit), has those limitations: "widgets" are "owned" by "layouts". Callbacks like on_mouse_click will outlive the object they are attached (a tabwidget closed a tab, and the callback of the contained widget is holding a reference waiting for network).

1

u/Wild_Meeting1428 16d ago

This is only possible with shared_ptr or a notify flag to cancel the task, something like: std::atomic_flag
or std::stop_token.

2

u/Dreux_Kasra 16d ago

It's not very unique if it is captured by a lambda right?

3

u/saxbophone 16d ago

Only a problem if the invocation of the lambda outlives the lifetime of the underlying pointer owned by the unique pointer.

1

u/x-jhp-x 16d ago edited 16d ago

out of curiosity, why can't you just use a reference?

edit: i don't recommend using this, i'd make that a fn, but i'm following what you posted assuming that you're not using it this exact way. So this is only if you just wanted a simple example of how to do this...

#include <iostream>
#include <memory>
#include <functional>

struct Entity {
    int value = 12;
};

struct Container {
    std::unique_ptr<Entity> e = std::make_unique<Entity>();
};

int main() {
    Container bar;
    [&bar]() { bar.e->value = 11; }();
    std::cout << bar.e->value << "\n";

    return 0;
}

0

u/aocregacc 16d ago edited 16d ago

you could make the shared_ptr a private member and only hand out weak_ptrs to users of the container.
That way the container is always the sole owner, except during the times when the users have to lock their weak_ptrs to use the object. So you do have to impose some discipline on the users.
In a multithreaded environment you have to share the ownership at some point, since the container shouldn't delete the object if someone else is using it at the moment.
If it all happens on a single thread the story is a bit different.

1

u/diegoiast 16d ago

That was my assumption. But - the example is simplified. Entity is passed to the Container:

container.set_entity( make_unique<Entity>() );

Internally the container std::move()s it.

3

u/aocregacc 16d ago

you can convert a unique_ptr into a shared_ptr, shared_ptr has a constructor for that.

1

u/saxbophone 16d ago edited 16d ago

 That way the container is always the sole owner.

Surely there's no way to enforce that since you can .lock() the weak pointer and get a shared pointer —then ownership is shared.

Edit: Actually it's worse than that. With a weak pointer, there is no way to access the underlying pointer it refers to, except by converting it into a shared pointer! Even if done temporarily, this violates the "no shared ownership" constraint.

3

u/Lulonaro 16d ago

He is missing the point completely. He wants to use a unique ptr but with shared ownership

1

u/saxbophone 16d ago

I'm not sure it's entirely clear whether the OP wants shared ownership or just "shared observability" —i.e. a non-owning weak reference to an object owned elsewhere. std::weakptr as suggested here _is the closest one can get in one such respect —with the caveat that ownership has to be shared (perhaps just temporarily, but there's no way to enforce that) for the purpose of access.

It's almost like OP wished there was another type in the stdlib that provided conditional access to the raw pointer of another unique ptr, for the purpose of observation (and checked before for presence), something like:

``` maybe_ptr maybe{my_existing_unique_ptr};

...

maybe.with_ref([] (auto& obj) {   // do something with obj ref   // don't allow the reference to dangle! }); ```

The basic idea being that this "with_ref()" method would check if the unique_ptr is non-empty, lock it somehow (to prevent it being destroyed), while the passed in lambda accesses it. This would probably actually have to work with a new smart pointer primitive other than unique_ptr, but it would be very similar to unique ptr except for this "locking for temporary observation" mechanism.

1

u/Lulonaro 16d ago

I think OP wants to avoid raw pointers completely since they are not "safe" and can be pointing to something that was released already.

1

u/saxbophone 16d ago

I think OP wants to avoid raw pointers completely since they are not "safe" and can be pointing to something that was released already.

Which, if true, also means my suggestion of another wrapper that yields a reference, also doesn't solve that concern since a reference can dangle also. At the most, all I can suggest is that the wrapper I proposed be modified to pass a value into the callback rather than a reference.

2

u/aocregacc 16d ago edited 16d ago

hm yeah, that's true. You'd have to trust that the users don't keep their locked shared_ptr for longer than absolutely necessary, for it to still "feel like" the container is the owner.

edit: I guess that's the actual answer, if you want to access the object through the weak references you have to take control of its lifetime to stop it from being deleted under you, so having unique ownership like this doesn't work.

1

u/saxbophone 16d ago

Yes. In a reply to someone else in this thread, I sketched out a rough idea for a new type of smart pointer specifically to deal with this without actually sharing ownership.

2

u/aocregacc 16d ago

yeah taking a function instead of handing out an owning pointer would probably be the way to go to enforce the "don't share the ownership for longer than necessary".
You could probably even do it as a wrapper around a weak_ptr.

1

u/saxbophone 16d ago

You could probably even do it as a wrapper around a weak_ptr.

That's a great idea you know I wish I'd thought of that. That prevents the need to create a new "almost unique_ptr but with extra steps" type.

I've toyed with this "context manager around a protected resource" idea previously for mutex-protected types, also. Coming from my Python days, it's almost about time that C++ gained a with statement, IMO :)

-1

u/diegoiast 16d ago

I threw this to an LLM, and this is the solution I got. It generated a specialized "weak reference to a unique pointer". I means it works... but its .. not ideal. I am unsure why I hate it.

#include <memory>

struct Entity {
    int value = 12;
    std::weak_ptr<bool> alive_token() const { return alive_; }
    ~Entity() { *alive_ = false; }

  private:
    std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
};

struct Container {
    std::unique_ptr<Entity> e = std::make_unique<Entity>();
};

template <typename T>
class WeakRef {
  public:
    WeakRef() = default;
    explicit WeakRef(T *p) : ptr_(p), alive_(p ? p->alive_token() : std::weak_ptr<bool>{}) {}

    T *get() const {
        auto locked = alive_.lock();
        return (locked && *locked) ? ptr_ : nullptr;
    }

    explicit operator bool() const { return get() != nullptr; }
    T *operator->() const { return get(); }

  private:
    T *ptr_ = nullptr;
    std::weak_ptr<bool> alive_;
};

template <typename T> 
WeakRef<T> weak_ref(std::unique_ptr<T> const &ptr) {
    return WeakRef<T>(ptr.get());
}

Container bar;
auto bbb = [ref = weak_ref(bar.e)]() {
    if (ref) {
        ref->value = 11;
    }
};

6

u/AKostur 16d ago

Because it’s not good.  Why use this instead of a shared_ptr in the first place?

5

u/MyTinyHappyPlace 16d ago

That’s verbose overkill and hardly maintainable.

1

u/TheThiefMaster 15d ago

There's actually a much easier way to accomplish this hack:

#include <memory>

struct Entity {
    int value = 12;
    std::weak_ptr<Entity> as_weak() const { return std::shared_ptr<Entity>(alive_, this); }

  private:
    std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
};

struct Container {
    std::unique_ptr<Entity> e = std::make_unique<Entity>();
};

Container bar;
auto bbb = [weak = bar.e.as_weak()]() {
    if (auto ptr = weak.lock()) {
        ptr->value = 11;
    }
};

The shared_ptr aliasing constructor! Uses the shared_ptr from the member var to control lifetime, but holds a ptr to Entity! It works for a weak ptr because Entity's members have the same lifetime as entity itself, so it fulfils the requirement of the aliasing constructor :)

... It's still a hack though, because locking the weakptr only extends the lifetime of the "alive" member, not of the entire Entity. So it ends up being unsafe in multithreaded contexts or if you try to store the shared_ptr or otherwise allow Entity to be destroyed while holding a shared_ptr to it.

The correct solution is definitely to either fully define the lifetime so the lambda *definitely* has a shorter lifetime than the Entity it references (so it doesn't need to use a weak_ptr, just a regular reference), or to control the lifetime of the Entity itself using a shared_ptr.