0

I am trying to understand the preferred approach for a class to handle the validity of (reference to) the object of another class.

In here, C has a vector that stores references of D objects. If D and C are a part of the library, how should C handle the case where a D object goes out of scope in the caller?

A couple approaches that I have in mind though not sure of the feasibility:

  • D knows about what _list stores and as soon as the said D goes out of scope, ~D() runs which removes itself from _list.
  • _list stores weak_ptr instead of a raw and upon any access to D, you invoke weak_ptr::lock() prior to accessing, though this will require the instantiation of a shared_ptr which seems to be not so common in production?
struct D
{
    ~D()
    {
        printf ("~D()\n");
    }
};

class C
{
    vector<D*> _list;

    public:
    void add(D* dObject)
    {
        _list.push_back(dObject);
        printf ("Adding D => size = %ld\n", _list.size());
    }

    ~C()
    {
        printf ("~C()\n");
    }
};

int main()
{
    C c1;
    {
        D d1;
        c1.add(&d1);
    }

    /**
     _list[0] is garbage now. How to avoid accessing it i.e 
      C being aware to not access it?
    */
    printf ("----out of scope---\n"); 
    D d2;                   
    c1.add(&d2);
}
xyf
  • 664
  • 1
  • 6
  • 16
  • 1
    `std::shared_ptr` is probably your best option. (Assuming that there is a strong necessity to "handle the validity".) I'm not a fan of `shared_ptr` (unless absolutely necessary) because I don't like federated ownership of a mutable variable, which is tantamount to having a global mutable variable. If it's a `shared_ptr` then that's okay, since Foo is const. – Eljay Nov 10 '22 at 23:57
  • 1
    You might want to be careful with your nomenclature. Pointers and smart pointers are not references. References are something else entirely in C++ (although it is possible to obtain a reference from a pointer, or a pointer from a reference, they have distinct characteristics and are not interchangeable). – Peter Nov 11 '22 at 00:49
  • @Eljay why `shared_ptr` over `weak_ptr` if `C` doesn't take any 'ownership' of `D`? ( – xyf Nov 11 '22 at 03:07
  • @Peter yes I understand the difference but should've been more careful with the words here. Thanks – xyf Nov 11 '22 at 03:07
  • My fault, when I said `std::shared_ptr`, I meant (implicitly) to also use `std::weak_ptr` for the non-owning pointers. I had assumed; I should have been explicit. – Eljay Nov 11 '22 at 03:28
  • gotcha. so something like this? https://godbolt.org/z/E7hEW1zr1. (added prints to help understand better hopefully). `_list` shall contain invalid objects and it's totally fine given we won't be causing UB unlike with raw pointers? – xyf Nov 11 '22 at 04:45
  • 1
    Yes, something like that is what I have in mind. However, again, I caution that (in my opinion) the `std::shared_ptr` is a solution of last resort. Because of my disdain for federated ownership, and it makes the lifecycle of the shared object more difficult to reason about, and that the object becomes tantamount to being a global variable. `shared_ptr` (with `weak_ptr`) can be used to model a single-owner relationship, but the constraint is by discipline/convention, not enforced by what the classes allow. – Eljay Nov 11 '22 at 14:08
  • 1
    You're taking the address of a local variable. Templates for managing pointers won't help you here - if this were inside another function (besides main) that was exiting, the stack storage would go away, taking the object with it. You should only be doing this with heap objects allocated with new. – dirck Nov 12 '22 at 03:01
  • which example are you referring to? are you implying `new` is always required for shared_ptr? – xyf Nov 12 '22 at 06:38
  • 1
    Yes, `new` is always required for `shared_ptr` - directly or indirectly, as in `std::make_shared` – dirck Nov 13 '22 at 01:49
  • Just to clarify, what'd you mean by indirectly? You're implying this uses `new` indirectly `auto sp = std::make_shared()`? – xyf Nov 13 '22 at 05:23
  • 1
    Yes, if you read the source or docs for make_shared, it explicitly creates a heap variable. – dirck Nov 13 '22 at 07:58
  • yes but how does that relate to the local variable going out of scope in your "answer", which I totally failed to understand. Or the wording of your answer is throwing me off specially when you say you can't use local variables. Like if unique_ptr allocates on the heap anyway so what's the concern and in what context are you saying this in regards to my original question? – xyf Nov 14 '22 at 05:50
  • I've made another edit to clarify my "answer". Hopefully it's enough information. – dirck Nov 14 '22 at 22:41

3 Answers3

1

Basically in c++, you should not keep any reference of the stack instance to some containers. It is dangerous with dangling memory accesses and you should add instances which is created on heap to your preferred container. In this case of, shared_ptr is useful and safe for managing the lifetime of your instance.

#include <vector>
#include <string>
#include <memory>
using namespace std;

struct D{
public:
    string _name;
    D(const string& name) : _name(name){}
    ~D(){
        printf("~D()\n");
    }
};

class C{
    vector<shared_ptr<D>> _list;
public:
    void add(shared_ptr<D> dObject){
        _list.push_back(dObject);
        printf("Adding D => size = %ld\n", _list.size());
    }
    void print() {
        for (const auto& d : _list) {
            printf("%s\n", d->_name.c_str());
        }
    }
    ~C(){
        printf("~C()\n");
    }
};

int main(){
    C c1;
    {
        c1.add(make_shared<D>("first D"));
    }
    c1.add(make_shared<D>("second D"));
    c1.print();
}

output:

Adding D => size = 1
Adding D => size = 2
first D
second D
~C()
~D()
~D()

Additional: remove an object by block scope

    C c1;
    {
        auto d = make_shared<D>("first D");
        shared_ptr<nullptr_t> D_deleter{ nullptr, [&](auto) { c1.remove(d); } };
        c1.add(d);
    } // d would be deleted here
shy45
  • 457
  • 1
  • 3
  • If the container is also on the same stack, adding a local variable to it can be safer, as both variables can be hopefully seen in context. – Sebastian Nov 16 '22 at 18:00
  • Yes I understand the example but `_list` still stores a dangling reference towards the end but it isn't a problem since it's not being accessed, which means `shared_ptr` will continue to store dangling references as well. Is that how it's typically done? – xyf Nov 18 '22 at 07:28
  • @xyf If you want to remove some of object when block scoped out, you can use the custom deleter of shared_ptr. See additional code above. In this case, deleter could remove even the reference/pointer of object on stack from c1 vector. – shy45 Nov 18 '22 at 14:22
1

Alternatives are

  1. Using scope_exit (also called scope guard):
int main() {
    C c1;
    {
        D d1;
        c1.add(&d1);
        scope_exit d1_erase([&] {
            c1.erase(&d1);
        });
        // ... insert here other code within the inner scope
    }
    // ... insert here other code within the outer scope
    return 0;
}

You should use a std::set instead of a std::vector within D to more efficiently find and erase entries again. And of course D should publicly provide an erase or delete function.

scope_exit is part of the soon-to-be-published C++ Library Fundamentals v3 Technical Specification (https://en.cppreference.com/w/cpp/experimental/scope_exit). The implementation belonging to the accepted proposal is here: https://github.com/PeterSommerlad/SC22WG21_Papers/tree/master/workspace/P0052_scope_exit.

  1. Creating a custom class managing adding and automatically deleting (also at scope exit) the pointer to and from the list from outside both classes:
class E
{
public:
    E(C& c, D* dp) : _c(c), _dp(dp) { _c.add(_dp); }; // add to list
    ~E() { _c.erase(_dp); };                          // remove from list

    E(const E&) = delete;            // rule-of-3
    E& operator=(const E&) = delete;

private:
    C& _c;
    D* _dp;
};

int main() {
    C c1;
    {
        D d1;
        E e(c1, &d1); // adds pointer to d1 to c1 list
                      // and removes it automatically at the end of the scope
    }

    return 0;
}

You could also store D within E instead of referring to it by stored reference.

BTW: Destruction order for local scopes is in reverse order to construction.

Those two solutions keep C and D as they are. You have to decide yourself, whether C intrusively should have knowledge of its D object or whether its addition and removal from D is handled non-intrusively like in those two solutions.

I would not use a shared_ptr/weak_ptr for those cases, where the objects are stored locally and the time of destruction is clear (when leaving scope either at its end or due to an exception or a return statement).

Sebastian
  • 1,834
  • 2
  • 10
  • 22
-1

Essentially, the original question is asking: "how do I know when a local variable goes out of scope?"

The answer is: when its destructor is called.

For the code in your question, you could have ~D() to remove itself from c1. This is awkward, and the discussion beneath the question brings up std::shared_ptr, but the code in the example uses a local variable. You can't use std::shared_ptr on a local variable, only on a heap variable. This is not made clear in the discussion.

Here are two references to questions specifically related to shared_ptr and local variables:

Create shared_ptr to stack object

and

Set shared_ptr to point existing object

A better solution to your problem is to not use a local variable, but a heap variable.

The difference between local and heap variables seems to be causing some confusion.

C++ has several storage classes for variables:

  • stack - used for local variables and function parameters
  • register - sometimes used for local variables - primarily an optimization
  • global/static - "one and only one", in space allocated at program load time.
  • thread local - global/static but thread specific - each thread has its own copy.
  • heap - allocated by the program, on the fly, via new or malloc (or equivalent). Sometimes called "free store".

C++ has some tricks with new and delete where you can use a custom heap, which is memory you're managing yourself, but that memory will have been allocated from the general heap, or possibly exist inside other variables.

Here are three different examples:

A local variable:

void some_function(C& c1)
{
    D d1;
    c1.add(&d1);
}

When this function exits, the stack frame that contains d1 goes away, and the variable ceases to exist. ~D() will get called, and you could remove d1 from c1 if you added a mechanism to do this, like making d1 hold a pointer to c1, etc., which is awkward, but possible.

std::shared_ptr will not help with a local variable.

A heap variable allocated with new:

void some_function(C& c1)
{
    D *d1p = new D();
    c1.add(d1p);
}

When this function exits, the D variable that d1p points to still exists. It will continue exist until it is explicitly deleted. In this context, the c1 variable "owns" this pointer, and it responsible for deleting it when it has finished using it.

This is the case where adding a smart pointer can simplify reference management, as in:

A heap variable and shared_ptr created via make_shared:

void some_function(C& c1)
{
    std::shared_ptr<D> d1p = std::make_shared<D>();
    c1.add(d1p);
}

This example creates a D variable on the heap, and creates a local shared_ptr holding a reference to that D variable, via a small counter object, which is also on the heap. When the shared_ptr is passed to the C::add method, the reference count in the counter object is incremented. When the shared_ptr goes out of scope, the reference count in the counter object is decremented. The shared_ptr in the c1 object now holds the only reference to that shared_ptr, and when the c1 object is destroyed, the shared_ptr it contains will be destroyed, releasing its reference - if the count goes to zero, the object it points to will also be destroyed.

Besides this automatic destruction, another advantage of this approach is that you can get references to this D variable from c1 and use them elsewhere, and these references (and the D variable itself) can outlive c1.

The C class has to be modified for this case, e.g.

    std::vector<std::shared_ptr<D>> _list;

and

    void add(std::shared_ptr<D> dObject)

Here's a reference to how shared pointers work:

How do shared pointers work?

and some documentation on make_shared:

https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared

dirck
  • 838
  • 5
  • 10
  • so you're saying `auto s = std::make_shared();` is completely wrong? I still fail to understand why would not using heap allocation be a problem. If the object gets deleted so be it. There's why `lock` exists for `weak_ptr` such that you only access the shared object if it's valid/not null. Sorry, downvoting cause it doesn't answer the question – xyf Nov 12 '22 at 06:39
  • `make_shared` uses new inside the template, that's no longer a stack variable. I've edited the answer to try and clear up some confusion. – dirck Nov 13 '22 at 01:41
  • The `scope_exit` technique from Sebastian's answer is good-to-know. My C++ usage is rusty. – dirck Nov 16 '22 at 17:02