0

Shared pointers are good idea, no doubt. But as long as a large scale program includes raw pointers, I think there is a big risk in using shared pointers. Mainly, you will loose control of the real life-cycle of pointers to objects that hold raw pointers, and bugs will occur in locations which are more difficult to find and debug.

So my question is, was there no attempt to add to modern c++ a "weak pointer" which does not depend on using shared pointers? I mean just a pointer which becomes NULL when deleted in any part of the program. Is there a reason not to use such a self-made wrapper?

To better explain what I mean, the following is such a "weak pointer" that I made. I named it WatchedPtr.

#include <memory>
#include <iostream>

template <typename T>
class WatchedPtr {
public:
    // the only way to allocate new pointer
    template <typename... ARGS>
    WatchedPtr(ARGS... args) : _ptr (new T(args...)), _allocated (std::make_shared<bool>(true)) {}

    WatchedPtr(const WatchedPtr<T>& other) : _ptr (other._ptr), _allocated (other._allocated) {}

    // delete the pointer
    void del () {delete _ptr; *_allocated = false;}

    auto& operator=(const WatchedPtr<T> &other) { return *this = other; }

    bool isNull() const { return *_allocated; }

    T* operator->() const { return _ptr; }

    T& operator*() const { return *_ptr; }

private:
    T* _ptr;
    std::shared_ptr <bool> _allocated;
};

struct S {
    int a = 1;
};

int main () {
    WatchedPtr<S> p1;
    WatchedPtr<S> p2(p1);
    p1->a = 8;
    std::cout << p1.isNull () << std::endl;
    std::cout << p2.isNull () << std::endl;
    p2.del ();
    std::cout << p1.isNull () << std::endl;
    std::cout << p1.isNull () << std::endl;
    return 0;
}

Result:

1
1
0
0

-Edited-

Thank you all. Some clarifications following the comments and answers so far:

  • The implementation I presented for WatchedPtr is merely to demonstrate what I mean: a pointer that does not get the copy from external allocation, cannot be deleted externally, and becomes null if it is deleted. The implementation is knowingly far from perfect and was not meant to be perfect.
  • Problem with mix of shared_ptr and raw pointers is very common: A* is held as raw pointer, thus created at some point of the program and explicitly deleted at some point of the program. B holds a A*, and B* is held as shared_ptr, thus B* has vague lifespan. Thus B may live long after the deletion of A* that B holds.
  • The main usage of "WatchedPtr" in my mind is defensive programing. i.e. check for null and do the best thing possible for continuity (and a debug error). shared_ptr can do it, but in a very dangerous way - it will hide and delay the problem.
  • There can also be a design usage for "WatchedPtr" (very few and explicit "owners"), but this is not the main idea. For that indeed shared pointers are doing the job.
  • The intention of "WatchedPtr" is not for replacing all existing raw pointers in the program at once. It is not the same effort as replacing to shared_ptr, which IMHO has be done for the whole program at once. Which is unrealistic for large scale programs.
Doron Ben-Ari
  • 443
  • 3
  • 12
  • Note: Qt has `QPointer` which becomes `nullptr` when the pointed-to `QObject` is destroyed. – Jesper Juhl Apr 03 '19 at 16:40
  • 7
    If you're already changing your code to use a certain kind of pointer wrapper, then why not just change it to properly use proper smart pointers? – Michael Kenzel Apr 03 '19 at 16:43
  • 2
    This is not thread safe. Despite your use of shared pointer, one thread could be using the pointer while the other deletes it. Only once it has been deleted will a thread be told "no it doesnt exist anymore". Theres a window for UB here AFAICT. – Borgleader Apr 03 '19 at 16:44
  • 1
    You want raw pointers and not pass shared pointers around when it's not useful. The concept of weak raw pointer is moot if your design memory repsonsibility properly. – Matthieu Brucher Apr 03 '19 at 16:44
  • 5
    I'm curious on why you would want this. You basically have a raw pointer. Can you show an example of what you want to do? `unique_ptr`, `shared_ptr` and `weak_ptr` pretty much take care of all of the ownership mechanics you would need. – NathanOliver Apr 03 '19 at 16:44
  • Wait - you have a a smart pointer - that wraps a smart pointer and a raw pointer? This seems a little wasteful – UKMonkey Apr 03 '19 at 16:45
  • Your `operator=` calls itself recursively and will enter an infinite loop. Additionally, your design still seems to require lots of manual bookkeeping and safety checks. `WatchedPtr` appears to both own and not own the managed object, which will make it very difficult to use correctly. When should `WatchedPtr` free the managed object? If you rely on the user to manually free the resource using `del()`, then this simply isn't useful. Just use a `shared_ptr` and `weak_ptr`s until you have a really good reason not to. – alter_igel Apr 03 '19 at 17:11
  • 1
    Raw pointer use should ideally be reserved for "non-owning, observing only pointers". At least that's whay *I* try to enforce. – Jesper Juhl Apr 03 '19 at 17:18
  • 2
    An important safety feature of `std::weak_ptr` is that you lock it before using it, producing a `std::shared_ptr` for as long as you need to use the referred object. With your solution it's not possible to prevent the destruction of the pointed-to object. Even if that isn't a concern, it seems like you're implementing a half-measure. You're keeping raw owning pointers and implementing some of the nice things about `std::shared_ptr`. It doesn't solve the fundamental problem that you are still using raw owning pointers. The change doesn't do much to leave the code base better than you found it. – François Andrieux Apr 03 '19 at 17:22
  • Putting aside your implementation of `WatchedPtr`, it seems your primary concern is about having predictable lifetimes of objects and avoiding spaghetti code. To this end, you should simply use the following tools, in order of preference. 1) By default, use automatic storage, i.e local or member objects, by value. This should work for nearly everything you do and is essentially foolproof. 2) If not, use a `unique_ptr` if you need indirection, i.e. for lightweight move semantics or for polymorphism. 3) If not, use a `shared_ptr` when there is no single correct place to store a shared object – alter_igel Apr 03 '19 at 17:35
  • You should read the [GC handbook](http://gchandbook.org/). Even if C++ don't have any garbage collection (unless you consider reference counting as a GC technique) the terminology and the concepts related to GC are relevant. In practice, liveness of data is a *whole program* property – Basile Starynkevitch Apr 03 '19 at 18:24

3 Answers3

5

Weak pointers rely on notifications from the smart pointer infrastructure, so you could never do this with actual raw pointers.

One could imagine an extension of, say, unique_ptr which supported weak pointers, certainly. Presumably the main reason that nobody rushed in to implement such a feature is that weak pointers are already at the "Use refcounting and everything should just work" end of the scale, while unique_ptr is at the "Manage your lifetimes through RAII or you're not a real C++ programmer" end of the scale. Weak pointers also require there to be a separate control block per allocation, meaning that the performance advantage of such a WatchedPtr would be minimal compared to shared_ptr.

Sneftel
  • 40,271
  • 12
  • 71
  • 104
  • That depends. One of the things I dislike about the `shared_ptr` infrastructure is that they are thread safe by default (with no way to opt out of it). This means any ref count needs to go through a atomic like class which introduces barriers/other mechanisms which have a reasonable cost, espeically on _non_ x86/x64 architectures. So something in the middle is possible (and useful in speicalised circumstances) – Mike Vine Apr 03 '19 at 17:07
  • @MikeVine But when is this really a problem. How often do you change or transfer ownership? Typically this only happens if you pass an object from one thread to another (so a the beginning and end of a task). The cases where you need to acquire temporary ownership should also be rare if designed correctly. For most situations, your code should be designed in a way that if a function accepts an object as a parameter, that this function does not require ownership, and as of that passing the managed raw pointer has to be sufficient. – t.niese Apr 03 '19 at 17:16
2

I think there is a big risk in using shared pointers. Mainly, you will loose control of the real life-cycle of pointers to objects that hold raw pointers, and bugs will occur in locations which are more difficult to find and debug.

Then you say

just a pointer which becomes NULL when deleted in any part of the program.

Don't you see the contradiction?

You don't want to use shared pointer because the lifetime of objects are determined at runtime. So far so good.

However, you want a pointer that automatically becomes null when the owner deletes it. The problem is if the lifetime of your pointer is known, you should not need that at all! If you know when the lifetime of your pointer ends, then you should be able to remove all instances of that pointer, a have a mean to check if the pointer is dead.

If you have a pointer that you don't know when the owner will free it and have no way to check or no observable side effect for the point of view of the weak owner, then do you really have control over lifetime of your pointer? Not really.

In fact, your implementation rely on containing a shared pointer. This is enlightening in the sense that you need some form of shared ownership in order to implement a raw pointer that can have weak pointer to it. Then if you need shared ownership to implement a raw pointer with weak references, you are left with a shared pointer. That's why the existence of your proposed class is contradictory.

std::shared_ptr + std::weak_ptr is made to deal with the issue of "parts of your program don't know when the owner free the resouce". What you need is a single std::shared_ptr and multiple std::weak_ptr, so they know when the resource is freed. These classes have the infomation needed to check the lifetime of a variable at runtime.

Or if in the contrary you know the lifetime of your pointers, then use that knowledge and find a way to remove dangling pointers, or expose a way to check for dangling pointers.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
0

Reading the answers and comments, along with C++ Core Guidelines by Bjarne Stroustrup & Herb Sutter, I have come to the following answer:
When following the guidelines, there is no need for a "WatchedPtr" which involves "new" and "delete". However, a way to track the validity of a raw pointer taken from a smart pointer, is still in question for me, for debug/QA purposes.

In details:

Raw pointers should continue to be used. For various reasons. However, explicit "new" and "delete" should not. The cases of calling "new" and "delete" should all be replaced by shared_ptr/unique_ptr.
At the place where a raw pointer is currently allocated, there is no point in replacing it by "WatchedPtr".
If replacing a raw pointer to something else where it is allocated, it will be in most cases to unique_ptr, and on the other cases to shared_ptr. The "WatchedPtr", if at all, will continue from that point, built from the shared/unique pointer.

Therefor I have posted a somewhat different question.

Doron Ben-Ari
  • 443
  • 3
  • 12