c++graph-theory

object defined in a function survives after program comes out of the function


I'm implementing a graph with C++. After reading a C++ tutorial, I thought that any object defined locally will only live in the most local scope. Say we define an int i in function f; outside the function, i is no longer a valid object. As far as I know, variable i exists in the stack of function f. Once f is finished, its stack gets removed, and all the objects created inside will be destroyed (unless dynamically allocated, in which case they exist in the heap).

But somehow when I'm implementing a member function, addEdge, which is supposed to receive input of a reference of an edge (a user defined object), create another edge that has vertices swapped to indicate backward direction, and use another member function addEdgeOneWay to add this newly created Edge to an adjacency list. I thought this edge f created in function addEdge is not going to live after function is finished and thus, when the addEdgeOneWay function is called f will no longer exist and the program will crash due to dangling reference. But it didn't! It passed the test cases:

    SECTION("add edges 2 ways to graph"){
        Graph<int> g{};
        
        //add 3 vertices 0, 1, 2 to graph
        int dummyContent = -1;
        g.addVertex(dummyContent);
        g.addVertex(dummyContent);

        const Edge e {0, 1};

        g.addEdge(e);
        REQUIRE(g.getAdjList()[0].size() == 1);
        REQUIRE(g.getAdjList()[1].size() == 1);

        REQUIRE((*g.getAdjList()[0].begin()).getThisVertex() == 0);
        REQUIRE((*g.getAdjList()[0].begin()).getThatVertex() == 1);
        REQUIRE((*g.getAdjList()[1].begin()).getThisVertex() == 1);
        REQUIRE((*g.getAdjList()[1].begin()).getThatVertex() == 0);
    }
template <typename T> 
class Graph{
public:
//default constructor, only null graph can be constructed
Graph() {}

Graph(int numVertices, std::vector<T> &vertices)
    : m_numVertex{numVertices}
    , m_vertexList{vertices}
{}

//only add edge from this vertex to that vertex
void addEdgeOneWay(const Edge& e){
    int u {e.getThisVertex()};
    int initSize{static_cast<int>(m_adjList[u].size())};
    m_adjList[u].insert(e);
    int finalSize{static_cast<int>(m_adjList[u].size())};
    m_numEdge += (finalSize - initSize);
}

void addEdge(const Edge& e){
    this->addEdgeOneWay(e);
    const Edge f{e.getThatVertex(), e.getThisVertex(), e.getWeight()};
    this->addEdgeOneWay(f);
}

I wonder why this works? Somehow after adding this local edge to graph's adjacency list (a vector of sets), it lives out of scope. I wonder if it has anything to do with temporary object, or something else.


Solution

  • Actually, your local object f is copied into the set and this copy survives the destruction of the local object since it lives until the set itself is destroyed.

    If you have doubts, you can track constructor/destructor calls by instrumenting them. Here is a simplified example:

    #include <iostream>
    #include <set>
    
    struct Edge
    {
        int value=0;
    
        Edge(int v) : value(v)               { std::cout << "user constructor "          << this << "\n"; }
        Edge()                               { std::cout << "default constructor "       << this << "\n"; }
        Edge(Edge const& e) : value(e.value) { std::cout << "copy constructor (const&) " << this << " from " << std::addressof(e) << "\n"; }
        Edge(Edge     && e) : value(e.value) { std::cout << "copy constructor (&&) "     << this << "\n"; }
         
        ~Edge() { std::cout << "destructor " << this << "\n"; }
    
        // We need this operator for using Edge with std::set
        bool operator< (Edge const& other) const { return value<other.value; }
    };
    
    // We define a set of Edge.
    std::set<Edge> edges;
    
    auto addEdge ()
    {
        std::cout << "Entering addEdge\n";
        
        // We create an Edge instance that lives during execution of 'addEdge'
        std::cout << "Creating edge\n";
        Edge e (123);
        
        // We add the edge into the set, which will copy 'e
        std::cout << "Inserting edge\n";
        edges.insert (e);
    
        std::cout << "Leaving addEdge\n";
    }
    
    int main()
    {
        std::cout << "Entering main\n";
        addEdge();
        std::cout << "Leaving main\n";
    }
    

    that will produce something like

    Entering main
    Entering addEdge
    Creating edge
    user constructor 0x7ffc59030fdc
    Inserting edge
    copy constructor (const&) 0xcb0c6e0 from 0x7ffc59030fdc
    Leaving addEdge
    destructor 0x7ffc59030fdc
    Leaving main
    destructor 0xcb0c6e0
    

    Notes:

    Demo

    Note that you will have a copy because the object isn't already contained in the set, so if you try to insert it again, no new copy will be made.

    auto addEdge ()
    {
        std::cout << "Entering addEdge\n";
        
        // We create an Edge instance that lives during execution of 'addEdge'
        std::cout << "Creating edge\n";
        Edge e (123);
        
        // We add the edge into the set, which will copy 'e
        std::cout << "Inserting edge 1\n";
        edges.insert (e);
    
        std::cout << "Inserting edge 2\n";
        edges.insert (e);
    
        std::cout << "Leaving addEdge\n";
    }
    

    However, if you change the value of the object and insert it into the set, a second copy will be done:

    auto addEdge ()
    {
        std::cout << "Entering addEdge\n";
        
        // We create an Edge instance that lives during execution of 'addEdge'
        std::cout << "Creating edge\n";
        Edge e (123);
        
        // We add the edge into the set, which will copy 'e
        std::cout << "Inserting edge 1\n";
        edges.insert (e);
    
        std::cout << "Inserting edge 2\n";
        e.value = 456; // we modify the content of the object
        edges.insert (e);
    
        std::cout << "Leaving addEdge\n";
    }