Skip to content
SS · Siddhanth Sethi, home

← Work/002 · Shipped · 2026

GraphSTL

A zero-dependency graph library in the style of the STL: one templated graph type, custom iterators, and algorithms written as free functions.

GraphSTL example graphA directed weighted graph of six nodes, A to F. Dijkstra from A to F follows A, C, D, F.42103515ABCDEF
#include <GraphSTL/graph.h>#include <GraphSTL/graph_algorithms.h>using namespace custom_stl; graph<char, int, true> g;  // directedfor (char c : {'A','B','C','D','E','F'}) g.addNode(c);g.addEdge('A','B',4); g.addEdge('A','C',2);g.addEdge('B','D',10); g.addEdge('C','E',3);g.addEdge('C','D',5); g.addEdge('D','F',1);g.addEdge('E','F',5); auto [cost, path] = dijkstra(g, 'A', 'F');// cost = 8, path = A C D F

Problem

The C++ standard library has containers for sequences, maps and sets, but nothing for graphs. Graph code tends to be rewritten for each problem: an adjacency list of one shape here, a matrix there, and algorithms tied to that one representation.

GraphSTL is one graph type that behaves like an STL container, with the algorithms kept separate from it, the way std::sort is separate from std::vector.

Constraints

  • Header-only C++17 with zero dependencies: include two headers, or link the graphstl::graphstl CMake target. A vcpkg manifest and CMake package config ship with it. MIT licensed.
  • Node values can be any hashable T, weights any arithmetic type, and directedness is fixed at compile time.
  • Algorithms must work on every graph<T, Weight, IsDirected> instantiation, and must refuse graphs they don't apply to.
  • Builds under -Wall -Wextra -Wpedantic in CI.

Architecture

GraphSTL architectureYour code builds a graph<T, Weight, IsDirected> with addNode and addEdge. graph.h stores an adjacency list keyed by integer node IDs, with hash maps between node values and IDs, and exposes forward iterators. graph_algorithms.h holds the algorithms as free functions that read the graph through its public accessors; Kruskal's MST uses a disjoint-set union. A test runner exercises everything, and GitHub Actions builds it with g++ and with CMake.begin / endreadsYOUR CODEaddNode · addEdgeGRAPH.Hadjacency list · ID mapsNODE ITERATORSforward · constGRAPH_ALGORITHMS.Hfree functionsDSUunion-find for KruskalTESTS46 casesGITHUB CIg++ · CMake
━ the library━ tested and built in CI
graph<T, Weight = int, IsDirected = false>
Three hash maps: NodeID → value, value → NodeID, and NodeID → vector of {target, weight} edges. IDs come from a counter and are never reused.
NodeIterator / ConstNodeIterator
Forward iterators over (NodeID, value) pairs, so std::find_if and std::count_if work on a graph (both tested).
graph_algorithms.h
BFS, DFS, Dijkstra (binary heap) and a naive O(V²) Dijkstra, Bellman-Ford, A*, Floyd-Warshall, Kruskal, Kahn, Kosaraju and Tarjan, as templates over any graph. Cycle detection is a member function.
DSU
Disjoint-set union with path compression and union by rank, used by Kruskal's MST.
Build and packaging
An INTERFACE CMake target with install rules and a version file, options to build the tests, benchmark and demo, and a dependency-free vcpkg manifest.
Tests and CI
One test runner with 46 cases. Two GitHub Actions jobs: a plain g++ build that runs the tests and compiles the benchmark, and a CMake build that runs CTest.

Key decisions & trade-offs

Values on the outside, integer IDs on the inside

Callers write g.addEdge('A', 'B', 4) with their own values; a value → ID hash map turns that into an O(1) average lookup, and every algorithm works on plain size_t IDs.

Trade-offEach value is stored twice, and T must be hashable (std::hash<T> and ==).

Directedness is a template parameter

graph<T, W, true> and graph<T, W, false> are different types. addEdge adds the reverse edge for undirected graphs, if constexpr picks the right cycle-detection strategy, and Kahn's sort and Kosaraju static_assert that the graph is directed, so misuse is a compile error.

Trade-offDirectedness can't change at runtime, and the two kinds can't be mixed in one container.

Algorithms are free functions in their own header

Like std::sort over iterators, the algorithms sit outside the container, so adding one never touches the graph class. They read the graph through getAdjList() and getData().

Trade-offThose accessors return the internal maps (as const references), so the representation is effectively part of the public API.

Results as values, unpacked with structured bindings

auto [cost, path] = dijkstra(g, 'A', 'F'); returns everything in one value. Unreachable targets come back as {numeric_limits<Weight>::max(), {}}, a negative cycle in Bellman-Ford as {min(), {}}, and Kahn's sort on a cyclic graph as an empty vector.

Trade-offThe sentinels are conventions the caller has to know; std::optional or a status field would make them impossible to miss.

Two Dijkstras, kept on purpose

The main version uses a binary-heap priority queue with lazy deletion (stale entries are skipped) and stops at the target. dijkstra_naive scans every node, O(V²), and exists so the benchmark can compare the two and check they agree.

Trade-offA second implementation to keep correct.

Failure modes handled

A node that doesn't exist

Lookups by value throw std::runtime_error("Node not found") rather than inventing a node. Adding an existing value returns its existing ID instead of creating a duplicate.

No path to the target

Dijkstra, A* and Bellman-Ford return the sentinel cost with an empty path; Floyd-Warshall's path() returns an empty vector. Each case is tested.

Negative weights and negative cycles

Bellman-Ford handles negative weights and reports a reachable negative cycle; Floyd-Warshall sets hasNegativeCycle. Both are tested.

A cycle where a DAG is required

Kahn's sort returns an empty vector when it can't order every node, tested together with cyclePresent().

The wrong kind of graph

Kahn's sort and Kosaraju's SCC on an undirected graph fail at compile time with a readable static_assert message.

Removing nodes and edges

removeNode also deletes every incoming edge from every other adjacency list; removeEdge on an undirected graph removes both directions. Both are tested, as is a self-loop counting as a cycle.

Runaway path reconstruction

Floyd-Warshall's path() walks the next-hop table at most |V| + 1 steps, so a corrupt table can't loop forever. Bellman-Ford stops as soon as a pass changes nothing.

What I'd do differently

Iterate instead of recursing

Tarjan, Kosaraju and cycle detection recurse through std::function lambdas, so a long chain of nodes can overflow the stack. Explicit stacks, as dfs() already uses, would remove the limit and the indirect call per visit.

Guard every precondition the way Kahn does

Dijkstra and A* silently return wrong answers on negative weights, and Kruskal accepts a directed graph and treats its edges as undirected. A static_assert(!IsDirected) on Kruskal and a negative-weight check would match the static_assert already on Kahn and Kosaraju.

Benchmark with repetitions and full runs

Each case is timed once with no warm-up, and both Dijkstras stop early at node n − 1. That measures early exit on a dense random graph more than the O((V+E) log V) versus O(V²) gap. Repeated runs, or Google Benchmark, over full single-source searches would show the real curve.

Store adjacency in vectors

NodeIDs are dense counters, yet adjacency and every algorithm's dist and parent use hash maps. Vectors indexed by ID would be faster and simpler; removed nodes would need a free list.

Property tests for every algorithm

A* and Kruskal have one test each. This site's TypeScript port cross-checks 200 random graphs (the shortest-path algorithms agree, Kruskal equals a brute-force Prim, Tarjan equals Kosaraju), and the same idea would fit the C++ suite.

Results

46 / 46
Tests passing
GitHub Actions (g++ and CMake/CTest), and re-run for this page with Apple clang 17 at -O2.
1.4–4.1×
Heap Dijkstra vs naive
The repo's benchmark, run three times for this page on an Apple M4 at -O2: 1.4–1.6× at 100 nodes, 1.8–2.2× at 500, 3.3–4.1× at 1,000. Costs matched in every case.
12
Algorithms
11 free functions in graph_algorithms.h plus cyclePresent().
0
Dependencies
Standard library only; the vcpkg manifest lists none.

Stack

  • C++17
  • CMake
  • vcpkg
  • GitHub Actions

Links