Reputation: 3
I would like how to keep some track of my differents pointers (objects) . For example let's assume this code (The language does not matter i had the same problem in C++ as well as in Python):
int *a = new int(2);
int *b = a;
delete a;
a = nullptr;
I would like to have a mean to know that b is not usable anymore. I thought about using a wrapper but this solution lack of elegance in my opinion.
Do you guys know a better solution , like some kind of bookkeeping techniques.
Thanks !!!
Upvotes: 0
Views: 1319
Reputation: 72401
The language does in fact matter, because some languages may have ways of doing what you want, depending on how they define and implement ideas such as objects and object lifetimes. And in fact many languages make it impossible or near impossible to have something similar to a dangling pointer or dangling reference. But the question is tagged C++ and has a C++ example, and C++ definitely does not have a way of doing this sort of check after a delete
expression.
Why not? Because one of the core principles in the C++ language and its typical compilers is that you don't pay for what you don't use. And on almost all computer architectures, C++ raw pointers can be implemented in a way that very efficiently gets at the memory, but doesn't provide any way to guarantee or check that the memory is actually still valid and not reused for something entirely different. A compiler could automatically add extra bookkeeping to make it possible to check whether a C++ pointer object is valid or not, but that would mean unavoidable extra work for the actual program execution that would significantly slow down all programs, even in parts that aren't using those sort of checks. So C++ can result in fast code in this way, but involves a danger in requiring the program to use pointers correctly.
On the other hand, C++ does make it reasonably easy to implement the sorts of wrappers and bookkeeping that do provide extra features like the checks you want, and still use a syntax quite similar to the familiar raw pointer syntax. Even better, some of the most useful patterns for those wrappers are provided for you in the Standard Library. A few ways we might rewrite your example code, plus a statement to actually attempt to use *b
:
Using std::unique_ptr
std::unique_ptr<int> a = std::make_unique<int>(2);
std::unique_ptr<int> b = a;
a = nullptr;
std::cout << *b;
Result: The program won't compile!
The initialization of b
from a
is illegal, because the "unique" in unique_ptr
means that the compiler is going to help you make sure there's only one pointer at a time to the owned object. Not for the situation you asked about, but it's actually quite common to need dynamically created objects but only one pointer to each, which also controls the lifetime of the object. In that case, one nice thing about unique_ptr
is that it helps avoid accidentally creating other pointers which might end up dangling later.
Another nice thing about it is that you don't need any equivalent of delete
. If the lifetime of the unique_ptr
ends (e.g. by reaching the end of a {
block}
, destroying an object which has a unique_ptr
as a member, or by removing a unique_ptr
from a container), it will do the delete
for you. Similarly, if you reassign the unique_ptr
to point at something else, it will delete
anything it was previously pointing at. So the a = nullptr;
line is a way of forcing the pointer to immediately clean up and forget its object, but it's not usually necessary. All this helps avoid leaks, double deletes, and dangling pointers.
And by the way, since unique_ptr
just does compile-time checks to avoid copies and automatic cleanup at well-defined points, programs that use it can usually be compiled into executables that are just as fast as programs that use raw pointers, new
, and delete
directly. Just with less work and danger for the programmer.
Using std::shared_ptr
std::shared_ptr<int> a = std::make_shared<int>(2);
std::shared_ptr<int> b = a;
a = nullptr;
std::cout << *b;
Result: Prints 2.
shared_ptr
adds bookkeeping to a pointer so that it always knows how many pointers to the object exist. The object normally gets destroyed as soon as and only when no shared_ptr
pointers to it exist any more.
But sometimes you don't want your second copy to actually keep the object alive. When you change the original pointer, the object should go away, like in your original code. That brings us to the example most like the original code:
Using std::shared_ptr
and std::weak_ptr
std::shared_ptr<int> a = std::make_shared<int>(2);
std::weak_ptr<int> b = a;
a = nullptr;
if (std::shared_ptr<int> b_lock = b.lock())
std::cout << *b_lock;
else
std::cout << "b is null\n";
Result: Prints "b is null".
weak_ptr
works together with shared_ptr
. When using both, it's still true that the object the pointers point at normally gets destroyed as soon as and only when no shared_ptr
points at it any more. But also, when that object is destroyed, any weak_ptr
pointers which were pointing at it automatically change to act like null pointers.
The b.lock()
call and shared_ptr b_lock
are needed because you can't use a weak_ptr
directly, as with *b
. This is a safety feature, because if you wrote code to check that b
is not null and then later uses *b
, what if some function you call between the check and the use (or some other thread!) happens to destroy or change a
? Instead we use lock()
to convert the weak_ptr
back to a local shared_ptr
, check whether that pointer is null, and use the shared_ptr
. The local shared_ptr
guarantees the object will continue living long enough for the code that's about to use it, but doesn't need to stick around after that.
So there you have your way of checking if a pointer is still valid.
Using shared_ptr
and weak_ptr
does require some thought about the conditions that should cause the objects they point at to stay alive or allow them to be cleaned up, but so would a design using new
and delete
directly.
Upvotes: 4