Skip to main content

Graaf Architecture

From a very high level, the project is structured in two parts:

  • The graph classes and core data structures
  • Algorithms and additional functionality

Graph classes and core data structures

The main class of the library is the abstract graph class:

enum class edge_type { WEIGHTED, UNWEIGHTED };
enum class graph_spec { DIRECTED, UNDIRECTED };

template <typename VERTEX_T, typename EDGE_T, edge_type EDGE_TYPE_V, graph_spec GRAPH_SPEC_V>
class graph {...};

An instance of a graph can have user provided types for the vertices and edges. Internally, it stores the graph in an adjacency list, and has separate containers for the vertex and edge instances:

// N.B. These types are a bit more abstracted in the codebase behind using
// declarations, but for clarity I have left this out.

// Adjacency information is stored in a set for fast existence checks and fast removal
std::unordered_map<vertex_id_t, std::unordered_set<vertex_id_t>> adjacency_list_{};

// Storing these in a separate container has the advantage that
// vertices and edges are only in memory once
std::unordered_map<vertex_id_t, VERTEX_T> vertices_{};
std::unordered_map<std::pair<vertex_id_t, vertex_id_t>, edge_t, edge_id_hash> edges_{};

The graph class is abstract as it contains pure virtual private methods related to the handling of edges (do_has_edge, do_get_edge, do_add_edge, and do_remove_edge).

Directed and undirected graphs

There are two classes which publicly derive from graph:

  • directed_graph
  • undirected_graph
template <typename VERTEX_T, typename EDGE_T, edge_type EDGE_TYPE_V = edge_type::UNWEIGHTED>
class directed_graph final
: public graph<VERTEX_T, EDGE_T, EDGE_TYPE_V, graph_spec::DIRECTED>
{...};

template <typename VERTEX_T, typename EDGE_T, edge_type EDGE_TYPE_V = edge_type::UNWEIGHTED>
class undirected_graph final
: public graph<VERTEX_T, EDGE_T, EDGE_TYPE_V, graph_spec::UNDIRECTED>
{...};

These are the classes which the user instantiates.

They provide implementations for the pure virtual methods related to handling edges. The unweighted_graph first sorts the pair of vertex ids related to an edge before interacting with the internal edges_ data structure. This ensures that an edge a->b is the same as an edge from b->a.

Weighted graphs

Certain algorithms (such as A*) operate on weighted graphs. A graph is automatically weighted if a primitive numeric type is passed as a template parameter to EDGE_T. Alternatively, user provided edge classes can publicly derive from weighted_edge.

The weighted_edge class provides a default implementation for the get_weight method, but this can be overridden in the derived class:

template <typename WEIGHT_T = int>
class weighted_edge {
public:
using weight_t = WEIGHT_T;
/**
* By default an edge has a unit weight.
*/
[[nodiscard]] virtual WEIGHT_T get_weight() const noexcept { return 1; };
};

To create an unweighted graph, simply do not derive from weighted_edge in your edge class.

Algorithms and additional functionality

The idea here is to keep the graph classes as general-purpose as possible, and to not include use case specific logic ( such as dot serialization) as member functions. Therefore, each algorithm/utility function is implemented as a free function.