-1

I am assigned to write a c++ graph implementation based on a given graph.h file. I am having an issue of "exception thrown: red access violation" in my AddEdge function that I cannot figure it out. Here is the description of graph.h

ifndef GRAPH_H

#define GRAPH_H

class GraphEdgeNotFound {  };                       // Exception class represents edge-not-found condition

#include <cstddef>
#include <new>
#include <iostream>
#include <iomanip>
#include <stack>                                    // For STL stack
#include <queue>                                    // For STL queue
#include <string>
using namespace std;


struct VertexNode;                                  // Forward declaration of VertexNode type

struct EdgeNode                                     // Structure representing an edge
{
  VertexNode*   destination;                        // Pointer to destination vertex
  int           weight;                             // Edge weight
  EdgeNode*     nextPtr;                            // Pointer to next edge
};

struct VertexNode                                             // Structure representing a vertex
{
  string        vname;                              // Name of vertex
  bool          mark;                               // Marked flag
  EdgeNode*     edgePtr;                            // Pointer to list of outgoing edges
  VertexNode*   nextVertex;                         // Pointer to next vertex in vertices list
};

class Graph                                         // Graph ADT using adjacency list representation
{
 private:       //***** Private class members below *****//
  VertexNode*   vertices;                       // Linked list of vertex nodes

 public:           //***** Public members below *****//
  // ... there are more here, I just focus on the AddEdge function

  void AddEdge(string s, string d, int w);      
  // AddEdge()
  // Adds edge from source S to destination D with specified weight W.

  VertexNode*  WhereIs(string v);       
  // WhereIs()
  // Returns pointer to the vertex node that stores vertex v in the vertices linked list; 
  // Throws GraphVertexNotFound if V is not present in the vertices list
}

And here is my current work (graph.cpp)

#include "graph.h"

void Graph::AddEdge(string s, string d, int w)
{
    // Adds edge from source S to destination D with specified weight W.
    // If there is not enough memory to add the edge, throw the GraphFull exception

    VertexNode* Vertex_S = WhereIs(s);
    VertexNode* Vertex_D = WhereIs(d);

    // Initialize newEdge
    EdgeNode* newEdge = new EdgeNode;
    newEdge->destination = Vertex_D;
    newEdge->weight = w;
    newEdge->nextPtr = NULL;

    if (Vertex_S->edgePtr == NULL)
    {
        Vertex_S->edgePtr = newEdge;
    }
    else
    {
        // Go the the end of edgePtr and add the newEdge to it 
        while (Vertex_S->edgePtr->nextPtr != NULL)
        {
            Vertex_S->edgePtr = Vertex_S->edgePtr->nextPtr;
        }
        Vertex_S->edgePtr->nextPtr = newEdge;
    }
}

VertexNode* Graph::WhereIs(string v)
{
    // Returns pointer to the vertex node that stores vertex v in the vertices linked list; 
    // Throws GraphVertexNotFound if V is not present in the vertices list

    VertexNode* tempVertex = vertices;

    // If found
    while (tempVertex != NULL)
    {
        if (tempVertex->vname == v)
            return tempVertex;  // Found
        tempVertex = tempVertex->nextVertex;
    }
    
    // If not found
    throw GraphVertexNotFound();
}

The issue I have is that when I debug it, I got an error at line while (Vertex_S->edgePtr->nextPtr != NULL) saying,

Exception thrown: read access violation.
Vertex_S->**edgePtr** was 0xCDCDCDCD

What did I do wrong?

Thank you for your help!

  • 2
    Just because this is where the program crashes or reports an error doesn't mean that's where the problem is. C++ does not work this way. The problem can be anywhere in your code, but after the bug occurs the program keeps running for a little bit before it finally crashes here. This is why stackoverflow.com's [help] requires you to show a [mre] that anyone can cut/paste ***exactly as shown***, then compile, run, and reproduce your problem. See [ask] for more information. Until you do that, it is unlikely that anyone will be able to figure out your problem. – Sam Varshavchik Dec 03 '20 at 23:23
  • 1
    [0xCDCDCDCD](https://stackoverflow.com/a/8275454) – Quimby Dec 03 '20 at 23:25
  • 1
    *// Assume WhereIs() function works properly* -- How do we know you're telling the truth? I have seen a lot of questions start of with assumptions like this, and when the code is finally shown, the code that is supposed to "work properly" was broken. – PaulMcKenzie Dec 03 '20 at 23:31
  • 1
    @PaulMcKenzie Hi Paul, I just realized that me trying to not post too much code is not a good idea in this case. Yes, I added the `WhereIs()` function in my original post. – Tony Nguyen Dec 04 '20 at 02:41
  • I hope that header file can be changed, it's idiotic. How could a teacher post this as an "excample" of anything good? All those unnecessary includes, the `using namespace std`, the passing of strings by value and not by const reference, the use of manual memory management, the inane comments (why would you comment "private stuff here" if there's clearly a `private` keyword - comments aren't for paraphrasing, they are to explain unobvious stuff!)... It's yet another case of teachers who don't know anything :( – Kuba hasn't forgotten Monica Dec 04 '20 at 02:44

2 Answers2

1

Your code appears to be correct, in that the the error is most definitely elsewhere. But you never free the linked list nodes within the destructors, so that's an error waiting to happen. Destroying an object should release all resources held by it, so we need to implement destructors.

Minimal Improvements

But let's also make it a little bit saner - here's the minimum I'd do to clean up the code and make it more robust (so that it reads just a tiny bit more like C++ and not C). I've commented the changes - there's lots of them. They are minor by themselves, but together they make for more idiomatic use of C++, and also make your life easier.

Common issue: using namespace std; is a very bad idea, and the little function below illustrates why. You'd hope such code won't compile. But it does.

#include <ios>
using namespace std;
void this_is_a_bug_and_it_happily_compiles()
{
    if (left != right)
        throw logic_error(
            "How could this even happen? What is left, what is right?!?!?!\n"
            "Clearly, using namespace std is pure evil!\n");
}

In the code below, the // comments are my comments that explain what I changed and what you should look for when writing such code. The only comments that should be left in, that I'd expect to be helpful in the long run, use the /* */ form.

This code at least compiles, and I hope it's correct (or at least will throw/assert if something is amiss).

graph.h

// This is a widely supported pragma and is easier to use than 
// legacy include guards.
#pragma once

// Only include what's needed.
#include <stdexcept>
#include <string>

// Use standard exceptions when creating custom exceptions - they convey
// some information about the meaning of the error.

class GraphEdgeNotFound : public std::runtime_error {
public:
    GraphEdgeNotFound();    
};

class GraphVertexNotFound : public std::runtime_error {
public:
    GraphVertexNotFound();
};

class GraphFull : public std::runtime_error {
public:
    GraphFull();
};

struct VertexNode;

// Don't comment obvious stuff: it's clear that this is a  "structure"
// representing an "edge" "node".
//
// When commenting, don't paraphrase what the basic c++ already says:
// there's no point to commenting EdgeNode *nextPtr as "pointer to next edge":
// it's clear that it's a pointer to an edge, and it's clear that it's pointer 
// to the next one - that's what proper typing and variable naming is for, and
// such comments are noise.

struct EdgeNode
{
  // Initialize all POD (plain old data) structure members to avoid bugs caused
  // by simple mistakes where members are left uninitialized.
  VertexNode*   to = nullptr; /* never null, the default value is not used */
  int           weight = 0;
  EdgeNode*     next = nullptr; /* owns the next node */

  // Use constructors to ensure that all members of the structure that shouldn't
  // have default values are initialized.
  // Use noexcept(false) to document that the function can indeed throw.
  // This signifies to the user that e.g. there may be a constraint on the
  // parameter.
  EdgeNode(VertexNode *to, int weight) noexcept(false);
  // A destructor must free the memory used the the nodes we own.
  ~EdgeNode();
  
  // A default constructor makes no sense: an edge must refer to some vertex
  EdgeNode() = delete;
  // Delete copy constructor and assignment since nodes cannot be copied
  EdgeNode(const EdgeNode &) = delete;
  EdgeNode &operator=(const EdgeNode &) = delete;
};

// Use variable names to convey meaning, e.g. `edgesOut` clearly means outgoing
// edges, vs. `edgePtr` that requires a comment.
//
// *Always* prefer to document things with code itself, rather than writing
// cryptic code that then needs comments. If the code needs comments, consider
// whether it could be written to make it clear.

struct VertexNode
{
  std::string   vname; /* not empty */
  bool          mark = false;
  EdgeNode*     edgesOut = nullptr; /* owns the edges */
  VertexNode*   next = nullptr; /* owns the next node */

  // Pass non-trivial input parameters like std::string by const reference, 
  // not by value.
  // It is OK to create a vertex without outgoing edges, so provide a default
  // value to signify that.
  // The constructor is explicit so that the compiler won't attempt to
  // convert a string to a vertex implicitly (when we least expect it).
  explicit VertexNode(const std::string &vname, EdgeNode *edgesOut = nullptr)
    noexcept(false);
  // A destructor must free the memory used the the nodes we own.
  ~VertexNode();
  
  // Again: no default constructor since such a vertex would be invalid
  VertexNode() = delete;
  // Delete copy constructor and assignment since nodes cannot be copied
  VertexNode(const VertexNode &) = delete;
  VertexNode &operator=(const VertexNode &) = delete;

  // Use member functions to encapsulate common operations on an object.
  void AppendEdge(EdgeNode *newEdge) noexcept(false);
};

class Graph
/* Graph ADT using adjacency list representation */
{
 private:
  VertexNode*   vertices = nullptr;

 public:
  // Use parameter names that make their purpose clear; if they need comments
  // then usually they are not named correctly!
  void AddEdge(const std::string &from, const std::string &to, int weight) noexcept(false);

  VertexNode* GetVertex(const std::string &vname) noexcept(false);
  /* Throws GraphVertexNotFound if no such vertex exist in the vertices list */
};

graph.cpp

#include "graph.h"
#include <cassert>

// Any global namespace inclusions should be put into the .cpp file, not .h.
// We still do not want `using namespace std`, instead we're enumerating all
// names that we will use. This may also help self-document which parts of
// the standard library are used. Only commonly used classes should be
// "pulled" into the global namespace here. The less common ones should be used
// with the `std::` prefix. **The only purpose here is to reduce some clutter.
// Prefer to use `std::` prefix by default.**

using std::invalid_argument;
using std::runtime_error;
using std::string;

GraphEdgeNotFound::GraphEdgeNotFound() : 
    runtime_error("Graph edge not found") {}

GraphVertexNotFound::GraphVertexNotFound() : 
    runtime_error("Graph vertex not found") {}

GraphFull::GraphFull() : 
    runtime_error("Out of memory while allocating graph") {}

// The common functionality of freeing linked lists should be factored out.
// It's a static function since we don't need it visible from outside this file.
template <typename T>
static void FreeList(T *node)
{
    while (node) {
        T *temp = node;
        node = node->next;
        temp->next = nullptr;
        delete temp; /* no recursion in temp's destructor: temp->next is null */
    }
}

// The above function doesn't do any recursive calling, because when the
// object is deleted, it doesn't own the successor node anymore.
// The following implementation would also "work" in that it would free the
// resources and wouldn't leak memory. But it would cause the destructors
// to recurse, as the parent node deletes the child which deletes the child and
// so on.
template <typename T>
static void do_not_use_such_FreeList(T *node)
{
    delete node;
}

EdgeNode::EdgeNode(VertexNode *to, int weight) :
    // Using this constructor with a null vertex is not
    // a runtime issue like a missing file. It's a bug in the code, and
    // there's no way to proceed since the rest of the code depends on the edges
    // actually pointing to a valid vertex. 
    // That's stated by the non_null validator below.
    to(to), weight(weight)
{
    // First Layer of Defense Against Dark Bugs: assert
    // In debug builds, this will drop us immediately to the debugger.
    // We have to debug this, it's a bug!
    assert(to);
    // Second layer of Defense Against Dark Bugs: throw
    // In release builds the assertions do nothing, so we must throw an exception
    // instead so that invalid operations don't take place.
    // We shouldn't assert here, since that loses the specific code line information
    // - remember that assert is a macro that records the line where it tripped.
    if (!to) throw invalid_argument("Attempt to create an edge to nowhere");
}

EdgeNode::~EdgeNode()
{
    FreeList(next);
}

VertexNode::VertexNode(const string &vname, EdgeNode *edgesOut) :
    vname(vname), edgesOut(edgesOut)
{
    assert(!vname.empty());
    if (vname.empty()) throw invalid_argument("Attempt to create a nameless vertex");
}

VertexNode::~VertexNode()
{
    // It's OK to invoke delete (and free!) with a null pointer. Do not check
    // for null when deleting/freeing memory this way - it's redundant.
    delete edgesOut;
    FreeList(next);
}

// Provides the reference to the last edge node pointer in the list
EdgeNode* &GetLastInList(EdgeNode *&head)
{
    EdgeNode **last = &head; /* points to pointer to an edge */
    while (*last)
        last = &((*last)->next); /* advance tail to the last edge */
    return *last;
}

// This is a generic function that can append an edge to any edge list.
// We'll probably use it in various places.
// Its name documents its purpose.
void AppendToEdgeList(EdgeNode *&head, EdgeNode *newEdge)
{
    assert(newEdge);
    if (!newEdge) throw invalid_argument("Attempt to append no edge to edge list");
    EdgeNode *&last = GetLastInList(head);
    last = newEdge;
}

// Appending edges to a vertex is a common thing: make it a function.
// Due to useful naming of various elements of code, this function
// documents itself - any comment here would be likely unnecessary and
// just paraphrase what the code already clearly states.
void VertexNode::AppendEdge(EdgeNode *newEdge)
{
    assert(newEdge);
    if (!newEdge) throw invalid_argument("Attempt to append no edge to the vertex");
    AppendToEdgeList(edgesOut, newEdge);
}

void Graph::AddEdge(const string &from, const string &to, int weight)
{
    assert(!from.empty());
    assert(!to.empty());
    if (from.empty() || to.empty())
        throw invalid_argument("Attempt to add an edge to/from an unnamed vertex");
    
    // The original code was not handling the required condition that
    // the out-of-memory condition should be handled.
    try
    {
        VertexNode *vFrom = GetVertex(from);
        VertexNode *vTo = GetVertex(to);

        EdgeNode* newEdgeTo = new EdgeNode(vTo, weight);
        vFrom->AppendEdge(newEdgeTo);
    }
    catch (std::bad_alloc) {
        throw GraphFull();
    }
}

VertexNode *Graph::GetVertex(const string &vname)
{
    assert(!vname.empty());
    if (vname.empty()) 
        throw invalid_argument("Attempting to get a vertex without a name");
        
    // The for loop collects all three elements of the loop
    // in self-documenting syntax: establishing the loop variable,
    // the termination condition, and the iteration step.
    // It's not always possible to collect everything inside a for(),
    // so it should be used when it makes things clearer, rather than
    // trying to shoehorn stuff into it.
    for (VertexNode* v = vertices; v; v = v->next)
    {
        if (v->vname == vname)
            return v;
    }

    throw GraphVertexNotFound();
}
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
0

Modern C++

The code can be certainly improved by getting rid of manual memory management and using unique pointers to automatically destroy the nodes, guaranteeing no memory leaks.

There are many more things that could be added, such as iterators for the tree, but the following code should give some idea of how to deal with unique_ptr and linked lists.

graph.h

#pragma once

#include <memory>
#include <stdexcept>
#include <string>

class GraphEdgeNotFound : public std::runtime_error {
public:
    GraphEdgeNotFound();    
};

class GraphVertexNotFound : public std::runtime_error {
public:
    GraphVertexNotFound();
};

class GraphFull : public std::runtime_error {
public:
    GraphFull();
};

struct VertexNode;
struct EdgeNode;

/* A custom deleter for std::unique_ptr to list nodes that prevents recursion */
namespace std {
    // We declare the deleter operators here, and implement them in graph.cpp.
    template <> struct default_delete<VertexNode> {
        void operator()(VertexNode *);
    };
    template <> struct default_delete<EdgeNode> {
        void operator()(EdgeNode *);
    };
}

struct EdgeNode
{
  // An edge can never refer to no vertex, so use a reference instead of
  // a pointer. In C++, a reference is a way of stating in code that
  // there always is some object to refer to, vs. a pointer that could be null.
  // There is no such thing as a "null reference".
  VertexNode&               to;
  int                       weight = 0;
  // An edge node forms a list and always owns the next list item.
  std::unique_ptr<EdgeNode> next;

  // The vertex is taken as a reference since it must be valid.
  EdgeNode(VertexNode &to, int weight);

  EdgeNode() = delete;
  EdgeNode(const EdgeNode &) = delete;
  EdgeNode &operator=(const EdgeNode &) = delete;
};

struct VertexNode
{
  std::string                 vname; /* not empty */
  bool                        mark = false;
  // The vertex owns its outgoing edges
  std::unique_ptr<EdgeNode>   edgesOut;
  // The vertex forms a linked list and owns its successor.
  std::unique_ptr<VertexNode> next;
  
  // Unique pointers represent ownership and cannot be copied, but can be
  // moved, thus we take the optional edges list by rvalue reference (&&)
  // and provide a default-constructed value ({}).
  explicit VertexNode(const std::string &vname,
                      std::unique_ptr<EdgeNode> &&edgesOut = {}) noexcept(false);

  VertexNode() = delete;
  VertexNode(const VertexNode &) = delete;
  VertexNode &operator=(const VertexNode &) = delete;

  // The edge is passed by rvalue reference, since owning pointers can only
  // be moved, not copied (there can be only one, unique owner). To make
  // it simpler to use the edge after it was appended, the reference to that
  // edge is returned. It's a reference since it's always valid.
  EdgeNode &AddEdge(std::unique_ptr<EdgeNode> &&newEdge) noexcept(false);
  // An overload that constructs a new edge from the arguments given 
  // - they are forwarded directly to the edge's constructor
  template <typename ...Args> EdgeNode &AddEdge(Args &&...args) {
      return AddEdge(std::make_unique<EdgeNode>(std::forward<Args>(args)...));
  }
};

class Graph
/* Graph ADT using adjacency list representation */
{
 private:
  std::unique_ptr<VertexNode> vertices;

 public:
  // The methods below all return a node by reference, since they either
  // succeed - and can return a valid node, or throw - and then don't return(!)
  
  VertexNode &AddVertex(std::unique_ptr<VertexNode> &&node);
  VertexNode &GetVertex(const std::string &vname) noexcept(false);
  /* Throws GraphVertexNotFound if no such vertex exist in the vertices list */

  // An overload that constructs a new vertex from the arguments given 
  // - they are forwarded directly to the vertex's constructor
  template <typename ...Args> VertexNode &AddVertex(Args &&...args) {
      return AddVertex(std::make_unique<VertexNode>(std::forward<Args>(args)...));
  }

  EdgeNode &AddEdge(const std::string &from, const std::string &to, int weight)
  noexcept(false);
};

graph.cpp

#include "graph.h"
#include <cassert>

using std::invalid_argument;
using std::runtime_error;
using std::string;
using std::unique_ptr;

GraphEdgeNotFound::GraphEdgeNotFound() : 
    runtime_error("Graph edge not found") {}

GraphVertexNotFound::GraphVertexNotFound() : 
    runtime_error("Graph vertex not found") {}

GraphFull::GraphFull() : 
    runtime_error("Out of memory while allocating graph") {}

/* Deletes list nodes without recursion in the destructor */
template <class T>
static void DeleteNode(T *ptr) {
    if (!ptr) return;

    unique_ptr<T> node = std::move(ptr->next);
    while (node) {
        std::unique_ptr<T> temp = std::move(node->next);
        assert(!node->next); /* no recursion - no successor */
        delete node.release();
        node = std::move(temp);
    }
    assert(!ptr->next); /* no recursion - no successor */
    delete ptr;
}

namespace std {
    void default_delete<VertexNode>::operator()(VertexNode *p) { DeleteNode(p); }
    void default_delete<EdgeNode>::operator()(EdgeNode *p) { DeleteNode(p); }
}

VertexNode::VertexNode(const string &vname, unique_ptr<EdgeNode> &&edgesOut) :
    vname(vname), edgesOut(std::move(edgesOut))
{
    assert(!vname.empty());
    if (vname.empty())
        throw invalid_argument("Attempt to create a nameless vertex");
}

EdgeNode::EdgeNode(VertexNode &to, int weight) :
    to(to), weight(weight)
{}

template <typename T>
static std::unique_ptr<T> &GetLastInList(std::unique_ptr<T> &head)
{
    auto *last = &head; /* points to pointer to a node */
    while (*last)
        last = &((*last)->next); /* advance to the last edge */
    assert(!*last); /* the last edge must be empty (tail of the list) */
    return *last;
}

template <typename T>
static T &AppendNodeToList(std::unique_ptr<T> &head, unique_ptr<T> &&newNode)
{
    assert(newNode);
    if (!newNode)
        throw invalid_argument("Attempt to append no node to a node list");
    
    auto &last = GetLastInList(head);
    auto *result = newNode.get(); /* save the pointer - it'll be null after the move */
    last = std::move(newNode); /* assign the new node to the tail */
    assert(!newNode); /* newNode was moved from */
    return *result;
}

EdgeNode &VertexNode::AddEdge(unique_ptr<EdgeNode> &&newEdge)
{
    assert(newEdge);
    return AppendNodeToList(edgesOut, std::move(newEdge));
}

EdgeNode &Graph::AddEdge(const string &from, const string &to, int weight)
{
    assert(!from.empty());
    assert(!to.empty());
    if (from.empty() || to.empty())
        throw invalid_argument("Attempt to add an edge to/from an unnamed vertex");
    
    try
    {
        VertexNode &vFrom = GetVertex(from);
        VertexNode &vTo = GetVertex(to);
        return vFrom.AddEdge(vTo, weight);
    }
    catch (std::bad_alloc) {
        throw GraphFull();
    }
}

VertexNode &Graph::AddVertex(std::unique_ptr<VertexNode> &&node)
{
    return AppendNodeToList(vertices, std::move(node));
}

VertexNode &Graph::GetVertex(const string &vname)
{
    assert(!vname.empty());
    if (vname.empty()) 
        throw invalid_argument("Attempting to get a vertex without a name");
        
    // The for loop collects all three elements of the loop
    // in self-documenting syntax: establishing the loop variable,
    // the termination condition, and the iteration step.
    // It's not always possible to collect everything inside a for(),
    // so it should be used when it makes things clearer, rather than
    // trying to shoehorn stuff into it.
    for (VertexNode* v = vertices.get(); v; v = v->next.get())
    {
        if (v->vname == vname)
            return *v;
    }

    throw GraphVertexNotFound();
}

main.cpp

And here's a little example of how the code above may be used.

#include <cassert>
#include "graph.h"

int main() {
    Graph graph;
    graph.AddVertex("vertex1");
    graph.AddVertex("vertex2");
    graph.AddVertex("vertex3");
    graph.GetVertex("vertex1");
    graph.GetVertex("vertex2");
    graph.GetVertex("vertex3");
    try {
        graph.GetVertex("vertex4"); // unknown vertex: throws
        assert(false); // won't run if preceding line had thrown
    } catch (GraphVertexNotFound) {
        assert(true);
    }
}
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Hi, Thank you for the advice. This is very helpful. However, this is an assignment that I have to strictly follow the give `graph.h` file (i.e. `graph.h` in not allowed to be modified) – Tony Nguyen Dec 04 '20 at 22:41
  • Just so you know: your teacher is teaching you C, not C++, and even as far as C with classes goes, it’s still pretty bad. What an incompetent teacher. May as well drop the pretense and tell you guys that it is indeed C that you are taught. The “Minimal Improvements” answer can be modified so that the header file is unchanged - you’ll externally use the structs, but internally in graph.cpp you’ll wrap them with real classes with constructors and destructors etc. Such details won’t change the external API. I’ll post a third answer that goes that way. – Kuba hasn't forgotten Monica Dec 04 '20 at 23:48
  • Hi Unslander, I was able to find out where the issue of `Exception thrown: read access violation. Vertex_S->**edgePtr** was 0xCDCDCDCD` is. It is in my destructor function, not in the part that I posted. Anyway, I was able to see a much better way to due with pointers in linked list from your answer! – Tony Nguyen Dec 05 '20 at 00:53