|
| Requirement | Minimum |
|---|---|
| C++ standard | C++20 |
| CMake | 3.20 |
| Compiler | GCC 13+, Clang 10+, or MSVC 2022+ |
Dependencies (Catch2, tl::expected) are fetched automatically by CMake.
include(FetchContent)
FetchContent_Declare(
graph3
GIT_REPOSITORY https://github.com/pratzl/desc.git
GIT_TAG main
)
FetchContent_MakeAvailable(graph3)
target_link_libraries(your_target PRIVATE graph::graph3)CPMAddPackage("gh:pratzl/desc@main")
target_link_libraries(your_target PRIVATE graph::graph3)git submodule add https://github.com/pratzl/desc.git extern/graph3add_subdirectory(extern/graph3)
target_link_libraries(your_target PRIVATE graph::graph3)cmake --preset linux-gcc-release
cmake --install build/linux-gcc-release --prefix /usr/localThen in your project:
find_package(graph3 REQUIRED)
target_link_libraries(your_target PRIVATE graph::graph3)graph-v3 works with your own containers out of the box.
A std::vector<std::vector<int>> is already a valid graph — no wrapper needed.
#include <graph/graph.hpp>
#include <iostream>
int main() {
// Each inner vector is a vertex's outgoing edges (target vertex IDs).
std::vector<std::vector<int>> g = {
{1, 2}, // vertex 0 → 1, 2
{2, 3}, // vertex 1 → 2, 3
{3}, // vertex 2 → 3
{} // vertex 3 (no outgoing edges)
};
std::cout << "Vertices: " << graph::num_vertices(g) << "\n";
for (auto&& u : graph::vertices(g)) {
auto uid = graph::vertex_id(g, u);
for (auto&& uv : graph::edges(g, u)) {
std::cout << uid << " -> " << graph::target_id(g, uv) << "\n";
}
}
}Output:
Vertices: 4
0 -> 1
0 -> 2
1 -> 2
1 -> 3
2 -> 3
For richer features — edge values, flexible container selection, partitioning —
use dynamic_graph with one of the 27 trait combinations.
#include <graph/container/dynamic_graph.hpp>
#include <graph/container/traits/vov_graph_traits.hpp>
#include <iostream>
using namespace graph;
using namespace graph::container;
// EV=double (edge value), VV=void, GV=void, VId=uint32_t
using Graph = vov_graph_traits<double>::graph_type;
int main() {
// Construct from an initializer list of {source, target, weight} edges.
Graph g({{0, 1, 1.0}, {0, 2, 4.0}, {1, 2, 2.0}, {2, 3, 3.0}});
std::cout << num_vertices(g) << " vertices\n";
for (auto&& u : vertices(g)) {
for (auto&& uv : edges(g, u)) {
std::cout << vertex_id(g, u) << " -(" << edge_value(g, uv) << ")-> "
<< target_id(g, uv) << "\n";
}
}
// The same loop using views with structured bindings:
for (auto&& [uid, u] : views::vertexlist(g)) {
for (auto&& [tid, uv, wt] : views::incidence(g, u, [](auto& g, auto& uv) {
return edge_value(g, uv);
})) {
std::cout << uid << " -(" << wt << ")-> " << tid << "\n";
}
}
}Output:
4 vertices
0 -(1)-> 1
0 -(4)-> 2
1 -(2)-> 2
2 -(3)-> 3
When you need maximum read performance on a static graph, use compressed_graph.
It stores the adjacency structure in Compressed Sparse Row (CSR) format — two flat
arrays (row offsets + column indices) that are contiguous in memory.
Benefits over dynamic_graph:
compressed_graph |
dynamic_graph |
|
|---|---|---|
| Memory layout | Two contiguous vectors (cache-friendly) | Vector of per-vertex edge containers |
| Iteration speed | Fastest — linear memory scan | Depends on edge container choice |
| Memory overhead | Minimal — no per-vertex container bookkeeping | Higher — each vertex owns a separate container |
| Mutability | Immutable after construction | Fully mutable |
| Best for | Analytics, read-heavy workloads, large graphs | Graph construction, dynamic updates |
#include <graph/container/compressed_graph.hpp>
#include <iostream>
using namespace graph;
using namespace graph::container;
// EV=int (edge weight), VV=void, GV=void
using Graph = compressed_graph<int>;
int main() {
// Same initializer-list construction as dynamic_graph: {source, target, value}
Graph g({{0, 1, 10}, {0, 2, 5}, {1, 3, 1}, {2, 1, 3}, {2, 3, 9}});
std::cout << num_vertices(g) << " vertices\n";
for (auto&& u : vertices(g)) {
for (auto&& uv : edges(g, u)) {
std::cout << vertex_id(g, u) << " -(" << edge_value(g, uv) << ")-> "
<< target_id(g, uv) << "\n";
}
}
}The same CPOs (vertices, edges, target_id, edge_value, …) and all
algorithms work identically on both container types — swap the type alias
and your code keeps working.
All algorithms live in include/graph/algorithm/.
Here we run Dijkstra's single-source shortest paths on a weighted graph.
#include <graph/container/dynamic_graph.hpp>
#include <graph/container/traits/vov_graph_traits.hpp>
#include <graph/algorithm/dijkstra_shortest_paths.hpp>
#include <iostream>
using namespace graph;
using namespace graph::container;
using Graph = vov_graph_traits<int>::graph_type;
int main() {
// 0 --10--> 1 --1--> 3
// | ^ ^
// 5 3 9
// | | |
// v | |
// 2 --------+--------+
Graph g({{0, 1, 10}, {0, 2, 5}, {1, 3, 1}, {2, 1, 3}, {2, 3, 9}});
std::vector<int> distance(num_vertices(g));
std::vector<vertex_id_t<Graph>> predecessor(num_vertices(g));
// Initialize distances to infinity, predecessors to self
init_shortest_paths(distance, predecessor);
// Run Dijkstra from vertex 0.
// The weight function extracts the edge value.
dijkstra_shortest_paths(
g, vertex_id_t<Graph>(0), distance, predecessor,
[](const auto& g, const auto& uv) { return edge_value(g, uv); });
for (size_t v = 0; v < distance.size(); ++v)
std::cout << "0 -> " << v << " : distance = " << distance[v] << "\n";
}Output:
0 -> 0 : distance = 0
0 -> 1 : distance = 8
0 -> 2 : distance = 5
0 -> 3 : distance = 9
Key points:
init_shortest_paths(distance, predecessor)sets distances to infinity and predecessors to self-indices.- The weight function
[](const auto& g, const auto& uv) { return edge_value(g, uv); }extracts the edge value as the weight. If omitted, all edges have weight 1. - There is also
dijkstra_shortest_distances()if you don't need predecessor tracking.
An edge list is a flat range of sourced edges — any
std::vector<std::pair<int,int>> or std::vector<std::tuple<int,int,double>>
qualifies automatically.
#include <graph/edge_list/edge_list.hpp>
#include <graph/graph.hpp>
#include <vector>
#include <iostream>
int main() {
// Unweighted edge list as pairs: {source, target}
std::vector<std::pair<int, int>> edges = {
{0, 1}, {0, 2}, {1, 3}, {2, 3}, {3, 3}
};
// Count self-loops using CPOs
int self_loops = 0;
for (auto&& uv : edges) {
if (graph::source_id(edges, uv) == graph::target_id(edges, uv))
++self_loops;
}
std::cout << "Self-loops: " << self_loops << "\n"; // 1
// Weighted edge list as 3-tuples: {source, target, weight}
std::vector<std::tuple<int, int, double>> weighted = {
{0, 1, 1.5}, {1, 2, 2.5}, {2, 3, 3.0}
};
double total = 0.0;
for (auto&& uv : weighted)
total += graph::edge_value(weighted, uv);
std::cout << "Total weight: " << total << "\n"; // 7.0
}Edge lists are useful for algorithms that operate on edges directly (e.g., Kruskal MST) and for bulk-loading data into adjacency list containers.
graph-v3 supports bidirectional edge access — you can query incoming edges
just as easily as outgoing edges. To enable incoming edges, construct a
dynamic_graph with the Bidirectional template parameter set to true:
#include <graph/container/dynamic_graph.hpp>
#include <graph/container/traits/vov_graph_traits.hpp>
using namespace graph;
using namespace graph::container;
// Bidirectional = true (sixth template parameter)
using Graph = dynamic_graph<void, void, void, uint32_t, true, true,
vov_graph_traits<void, void, void, uint32_t, true>>;With a bidirectional graph, you can use the incoming-edge CPOs:
Graph g({{0, 1}, {0, 2}, {1, 3}, {2, 3}});
// Incoming edges to vertex 3
for (auto&& e : in_edges(g, *find_vertex(g, 3))) {
std::cout << source_id(g, e) << " -> 3\n";
}
// Output: 1 -> 3
// 2 -> 3
std::cout << "In-degree of vertex 3: " << in_degree(g, *find_vertex(g, 3)) << "\n";
// Output: 2All search views (BFS, DFS, topological sort) accept an Accessor template
parameter that controls traversal direction. Pass in_edge_accessor to
traverse in reverse — following incoming edges instead of outgoing:
#include <graph/views/bfs.hpp>
#include <graph/views/edge_accessor.hpp>
using namespace graph::views;
// Forward BFS from vertex 0 (default — outgoing edges)
for (auto [v] : vertices_bfs(g, 0)) {
std::cout << vertex_id(g, v) << " ";
}
// Output: 1 2 3
// Reverse BFS from vertex 3 (incoming edges)
for (auto [v] : vertices_bfs<in_edge_accessor>(g, 3)) {
std::cout << vertex_id(g, v) << " ";
}
// Output: 1 2 0For the full guide, see Bidirectional Access.
- Adjacency Lists User Guide — concepts, CPOs, descriptors
- Edge Lists User Guide — edge patterns, vertex ID types
- Views — BFS, DFS, topological sort, incidence, neighbors
- Bidirectional Access — incoming edges, reverse traversal
- Container Guide —
dynamic_graph,compressed_graph,undirected_adjacency_list - Algorithm Reference — all 13 algorithms
- Migration from v2 — what changed from graph-v2
- FAQ — common questions