3
votes

I'm currently working on new project with my team in Cpp and we decide to say good bye to the old fashioned raw pointers and give the smart pointers a try.

I'm really new at this world and decided to try a few things that I'll need for our project before use them for real.

The thing is, I started with the basics, and then I've got stucked with this.

When I create two new (shared) pointers (see the code below) with the make_shared function, and then (cross) store them in the other's inner vector, neither of them get destroyed at the end of the program.

shared_ptr.cpp:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

using namespace std;

class Object{

public:

    vector<shared_ptr<Object>> list;
    string name;
    Object(string name){
        this->name=name;
    }
    ~Object(){
        cout<<"...Deleting "<<name<<endl;
    }
};

int main(){

    shared_ptr<Object> o1 = make_shared<Object>("o1");
    shared_ptr<Object> o2 = make_shared<Object>("o2");

    o1->list.push_back(shared_ptr<Object>(o2));
    o2->list.push_back(shared_ptr<Object>(o1));

}

compiler instructions

g++ shared_ptr.cpp -o shared.o && ./shared.o

My question here is, shouldn't the shared pointer handle the reference itself? Or should I be the one to clear the inside-vector before the program finished?

Maybe I just have to store raw pointers in the inner lists and just stick to the smart pointers in those sections where I do create and delete them.

I have to use a similar structure in my project, that's why I wanted to ask here first before frustrating and going back to the old (but effective) raw pointer.

Thanks!

2
Except in the very narrow circumstances, liberal usage of shared pointers in C++ code indicates either a bad design or the fact that people designing the system didn't want to spend time and think about object ownership and lifetime. Even with shared pointers, C++ is not Java, so one needs to be conscious about this stuff. Also, it is perfectly fine to use non-owning raw pointers. – SergeyA
Off-topic, but it’s unnecessary to make explicit copies when you push to the vectors. All it does is spend more time copying. – molbdnilo
What would you do if these were raw pointers? It should not be difficult to transform whatever you would do with raw pointers into equivalent code that handles the smart pointers. For example, would you have a collection of all instances of Object and "disconnect" them? – David Schwartz

2 Answers

2
votes

Shared pointers are reference counted pointers.

They are not "garbage collected pointers" and cannot be used arbitrarily as such. Specifically, reference counting cannot handle cyclic references: if A points to data that points to B's data and B points to data that points to A's data then if A goes out of scope A's content will not be destroyed because B still refers to it and if B then goes out of scope B's content will not be destroyed because A still refers to it.

Shared pointers model the common sense notion of shared "ownership", if A uniquely refers to B using a shared pointer, one can think of A as owning B. If A and C both refer to B with shared pointers then we can think of A and C as jointly owning B. If your data does not conform to a notion of ownership like this then you should not use shared pointers. In particular in generic directed graphs, individual vertices cannot be said to "own" their neighbors as generic graphs can have arbitrary topologies; they exhibit no natural hierarchy; there are no "parents" and "children" in arbitrary graphs. That is, such graphs can have cycles.

Directed acyclic graphs are a different story: their vertices can be ordered in what is called a "topological order" in which parents precede children. There is a sense in which groups of vertices do own other vertices.

This is not to say that there is no way to manage the memory allocated by the vertices of a general graph using smart pointers, just that you can't do it by treating its vertices as the shared owners of child vertices. You could for example create vertices with a factory that maintains references to what it creates as a collection of std::unique_ptrs and use raw pointers for references from vertex to vertex. Or you could maintain your graph as a DAG of shared_ptrs but use weak_ptrs for the "back pointers" from children to parents.

What design is the best depends on the specifics of what you are doing.

1
votes

My question here is, shouldn't the shared pointer handle the reference itself? Or should I be the one to clear the inside-vector before the program finished?

There are many solutions to that. Of course, a std::weak_ptr can break the cycle, but sometimes it's another issue.

Take this for example:

struct Graph {
    struct Node {
        std::array<std::shared_ptr<Node>, 4> adjacent;
    };

    std::vector<std::shared_ptr<Node>> nodes;
};

Yes, indeed, each node have reference to other node. And we don't want dangling, so shared it is... no?

No. clearly here, we have a owner of all nodes: the Graph class.

If we write the same code with Graph being the unique owner of the nodes, we get this:

struct Graph {
    struct Node {
        std::array<Node*, 4> adjacent;
    };

    std::vector<std::unique_ptr<Node>> nodes;
};

When the graph dies, it takes to nodes with it.

Buuuuuut! What happens when I do this:

Node n = *get_graph_copy().nodes[1];
n.adjacent[0]; // boom!

Well, you broke the contract. Does it really make sense to have a node without a graph?