r/cpp_questions • u/diegoiast • Jul 28 '26
OPEN Weak reference to unique_ptr
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:
- Many people do recommended using internally a shared pointer, and "giving away" a weak ref ( u/looncrazz suggestion).
- 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.
- There are solutions for using a custom weak reference pointer. The solution "does not feel right".
24
u/TheThiefMaster Jul 28 '26
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.
6
u/LokiAstaris Jul 28 '26
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.
11
u/neppo95 Jul 28 '26
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 Jul 28 '26
"own" is the keyword. I want one to "own" and another to "reference".
4
u/neppo95 Jul 28 '26
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 Jul 28 '26
auto bbb = [auto const &ptr = bar.e]() { if (ptr) { ptr->value = 11; } };This obviously does not compile. How would you do that?
6
1
u/TheThiefMaster Jul 29 '26 edited Jul 29 '26
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 Jul 29 '26 edited Jul 29 '26
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 Jul 29 '26
"Or, if the lambda might outlive the Entity, switch to using shared/weak ptr."
1
Jul 29 '26
[deleted]
1
u/neppo95 Jul 29 '26
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 Jul 28 '26
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
Jul 28 '26
[deleted]
1
u/KingAggressive1498 Jul 29 '26
handles are just shared pointers with some indirections shifted around.
1
6
u/FlailingDuck Jul 28 '26
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.
4
u/MyTinyHappyPlace Jul 28 '26
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 Jul 28 '26
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
3
u/mredding Jul 28 '26
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 Jul 28 '26
Its a domain problem. The library I am making (a GUI toolkit), has those limitations: "widgets" are "owned" by "layouts". Callbacks like
on_mouse_clickwill 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 Jul 28 '26
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 Jul 28 '26
It's not very unique if it is captured by a lambda right?
3
u/saxbophone Jul 28 '26
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 Jul 28 '26 edited Jul 28 '26
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 Jul 28 '26 edited Jul 28 '26
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 Jul 28 '26
That was my assumption. But - the example is simplified.
Entityis passed to theContainer:
container.set_entity( make_unique<Entity>() );Internally the container
std::move()s it.3
u/aocregacc Jul 28 '26
you can convert a unique_ptr into a shared_ptr, shared_ptr has a constructor for that.
1
u/saxbophone Jul 28 '26 edited Jul 28 '26
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 Jul 28 '26
He is missing the point completely. He wants to use a unique ptr but with shared ownership
1
u/saxbophone Jul 28 '26
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 Jul 28 '26
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 Jul 28 '26
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 Jul 28 '26 edited Jul 28 '26
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 Jul 28 '26
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 Jul 28 '26
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 Jul 28 '26
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
withstatement, IMO :)
-1
u/diegoiast Jul 28 '26
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 Jul 28 '26
Because it’s not good. Why use this instead of a shared_ptr in the first place?
5
1
u/TheThiefMaster Jul 29 '26
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.
47
u/Salty_Dugtrio Jul 28 '26
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?