- Overview
- Quick Start
- Basic Views — vertexlist, incidence, neighbors, edgelist
- Incoming Edge Views — in_incidence, in_neighbors
- Simplified Views (basic_) — id-only variants
- Search Views — DFS, BFS, topological sort
- Reverse Search (Accessor Parameter) — BFS/DFS/topo over incoming edges
- Range Adaptor Syntax
- Value Functions
- Chaining with std::views
- Performance Considerations
- Best Practices
- Common Patterns
- Limitations
Graph views provide lazy, range-based access to graph elements using C++20 ranges and structured bindings. Views are composable, support pipe syntax, and integrate seamlessly with standard library range adaptors.
Key Features:
- Lazy evaluation - elements computed on-demand
- Zero-copy where possible - views reference graph data
- Structured bindings - elegant
auto [v, val]syntax - Range adaptor closures - pipe syntax
g | view() - Standard library integration - chain with
std::views - Descriptor-based - value functions receive descriptors
#include <graph/graph.hpp>
using namespace graph;
// Create a graph (vector-of-vectors for simplicity)
std::vector<std::vector<int>> g(5);
g[0] = {1, 2};
g[1] = {2, 3};
g[2] = {3, 4};
g[3] = {4};
g[4] = {};
// Iterate over all vertices
for (auto [uid, u] : views::vertexlist(g)) {
std::cout << "Vertex: " << uid << "\n";
}
// Iterate over edges from vertex 0
for (auto [vid, uv] : views::incidence(g, 0)) {
std::cout << "Edge to: " << vid << "\n";
}
// DFS traversal from vertex 0
for (auto [v] : views::vertices_dfs(g, 0)) {
std::cout << "Visited: " << vertex_id(g, v) << "\n";
}Iterates over all vertices in the graph.
Signature:
auto vertexlist(G&& g);
auto vertexlist(G&& g, VVF&& vvf); // with value function
auto vertexlist(G&& g, first, last); // subrange by descriptor
auto vertexlist(G&& g, first, last, VVF&&); // subrange + value function
auto vertexlist(G&& g, vr); // vertex range
auto vertexlist(G&& g, vr, VVF&&); // vertex range + value functionStructured Bindings:
| Variant | Binding |
|---|---|
vertexlist(g) |
[uid, u] |
vertexlist(g, vvf) |
[uid, u, val] |
Example:
// Without value function
for (auto [uid, u] : views::vertexlist(g)) {
std::cout << "Vertex " << uid << "\n";
}
// With value function (graph passed as parameter, not captured)
auto vvf = [](const auto& g, auto& u) { return degree(g, u); };
for (auto [uid, u, deg] : views::vertexlist(g, vvf)) {
std::cout << "Vertex " << uid << " has degree " << deg << "\n";
}Iterates over outgoing edges from a specific vertex.
Signature:
auto incidence(G&& g, UID uid);
auto incidence(G&& g, UID uid, EVF&& evf); // with value functionStructured Bindings:
| Variant | Binding |
|---|---|
incidence(g, uid) |
[vid, uv] |
incidence(g, uid, evf) |
[vid, uv, val] |
Example:
// Without value function
for (auto [vid, uv] : views::incidence(g, 0)) {
std::cout << "Edge to " << vid << "\n";
}
// With value function (graph passed as parameter, not captured)
auto evf = [](const auto& g, auto& uv) { return edge_value(g, uv); };
for (auto [vid, uv, w] : views::incidence(g, 0, evf)) {
std::cout << "Edge to " << vid << " weight " << w << "\n";
}Iterates over adjacent vertices (targets of outgoing edges).
Signature:
auto neighbors(G&& g, UID uid);
auto neighbors(G&& g, UID uid, VVF&& vvf); // with value functionStructured Bindings:
| Variant | Binding |
|---|---|
neighbors(g, uid) |
[vid, n] |
neighbors(g, uid, vvf) |
[vid, n, val] |
Example:
// Without value function
for (auto [vid, n] : views::neighbors(g, 0)) {
std::cout << "Neighbor: " << vid << "\n";
}
// With value function (graph passed as parameter, not captured)
auto vvf = [](const auto& g, auto& v) { return vertex_value(g, v); };
for (auto [vid, n, val] : views::neighbors(g, 0, vvf)) {
std::cout << "Neighbor " << vid << " value: " << val << "\n";
}Iterates over all edges in the graph (flattened iteration).
Signature:
auto edgelist(G&& g);
auto edgelist(G&& g, EVF&& evf); // with value functionStructured Bindings:
| Variant | Binding |
|---|---|
edgelist(g) |
[uid, vid, uv] |
edgelist(g, evf) |
[uid, vid, uv, val] |
Example:
// Without value function
for (auto [uid, vid, uv] : views::edgelist(g)) {
std::cout << "Edge: " << uid << " -> " << vid << "\n";
}
// With value function (graph passed as parameter, not captured)
auto evf = [](const auto& g, auto& uv) { return edge_value(g, uv); };
for (auto [uid, vid, uv, w] : views::edgelist(g, evf)) {
std::cout << uid << " -> " << vid << " weight " << w << "\n";
}For graphs that support bidirectional edge access (e.g., dynamic_graph with
Bidirectional = true, or undirected_adjacency_list), the library provides
incoming-edge view variants.
Iterates over incoming edges to a specific vertex.
Signature:
auto in_incidence(G&& g, UID uid);
auto in_incidence(G&& g, UID uid, EVF&& evf); // with value functionStructured Bindings:
| Variant | Binding |
|---|---|
in_incidence(g, uid) |
[sid, uv] |
in_incidence(g, uid, evf) |
[sid, uv, val] |
Example:
// Incoming edges to vertex 3
for (auto [sid, uv] : views::in_incidence(g, 3)) {
std::cout << "Edge from " << sid << "\n";
}
// With value function
auto evf = [](const auto& g, auto& uv) { return edge_value(g, uv); };
for (auto [sid, uv, w] : views::in_incidence(g, 3, evf)) {
std::cout << "Edge from " << sid << " weight " << w << "\n";
}Pipe syntax:
using namespace graph::views::adaptors;
auto view = g | in_incidence(3);Iterates over vertices that have edges to a specific vertex (predecessor vertices).
Signature:
auto in_neighbors(G&& g, UID uid);
auto in_neighbors(G&& g, UID uid, VVF&& vvf); // with value functionStructured Bindings:
| Variant | Binding |
|---|---|
in_neighbors(g, uid) |
[sid, n] |
in_neighbors(g, uid, vvf) |
[sid, n, val] |
Example:
// Predecessors of vertex 3
for (auto [sid, n] : views::in_neighbors(g, 3)) {
std::cout << "Predecessor: " << sid << "\n";
}Pipe syntax:
using namespace graph::views::adaptors;
auto view = g | in_neighbors(3);Each basic view has a basic_ variant that returns ids only (no vertex/edge descriptors).
These are lighter-weight when you only need identifiers and don't need to access the
vertex or edge objects themselves.
| Standard | Simplified | Binding |
|---|---|---|
vertexlist → [uid, u] |
basic_vertexlist → [uid] |
vertex id only |
incidence → [vid, uv] |
basic_incidence → [vid] |
target id only |
neighbors → [vid, n] |
basic_neighbors → [vid] |
target id only |
edgelist → [uid, vid, uv] |
basic_edgelist → [uid, vid] |
source + target id |
All basic_ views support value functions, adding one extra binding element:
basic_vertexlist(g, vvf)→[uid, val]basic_incidence(g, uid, evf)→[vid, val]basic_neighbors(g, uid, vvf)→[vid, val]basic_edgelist(g, evf)→[uid, vid, val]
using namespace graph::views;
for (auto [uid] : basic_vertexlist(g)) { ... }
for (auto [vid] : basic_incidence(g, 0)) { ... }
for (auto [vid] : basic_neighbors(g, 0)) { ... }
for (auto [uid, vid] : basic_edgelist(g)) { ... }Search views perform graph traversal and yield vertices/edges in traversal order.
Depth-first search traversal yielding vertices or edges.
Signature:
auto vertices_dfs(G&& g, Seed seed);
auto vertices_dfs(G&& g, Seed seed, VVF&& vvf);
auto vertices_dfs(G&& g, Seed seed, VVF&& vvf, Alloc alloc);
auto edges_dfs(G&& g, Seed seed);
auto edges_dfs(G&& g, Seed seed, EVF&& evf);
auto edges_dfs(G&& g, Seed seed, EVF&& evf, Alloc alloc);Structured Bindings:
| Variant | Binding |
|---|---|
vertices_dfs(g, seed) |
[v] |
vertices_dfs(g, seed, vvf) |
[v, val] |
edges_dfs(g, seed) |
[uv] |
edges_dfs(g, seed, evf) |
[uv, val] |
Iterator category: input (single-pass).
Example:
// DFS from vertex 0
for (auto [v] : views::vertices_dfs(g, 0)) {
std::cout << "DFS visited: " << vertex_id(g, v) << "\n";
}
// DFS edges
for (auto [uv] : views::edges_dfs(g, 0)) {
auto src = source_id(g, uv);
auto tgt = target_id(g, uv);
std::cout << "Tree edge: " << src << " -> " << tgt << "\n";
}View accessors:
depth()— current stack depthnum_visited()— vertices visited so farcancel(cancel_search::cancel_all)— stop iteration immediatelycancel(cancel_search::cancel_branch)— skip current subtree
Breadth-first search traversal yielding vertices or edges.
Signature:
auto vertices_bfs(G&& g, Seed seed);
auto vertices_bfs(G&& g, Seed seed, VVF&& vvf);
auto vertices_bfs(G&& g, Seed seed, VVF&& vvf, Alloc alloc);
auto edges_bfs(G&& g, Seed seed);
auto edges_bfs(G&& g, Seed seed, EVF&& evf);
auto edges_bfs(G&& g, Seed seed, EVF&& evf, Alloc alloc);Structured Bindings:
| Variant | Binding |
|---|---|
vertices_bfs(g, seed) |
[v] |
vertices_bfs(g, seed, vvf) |
[v, val] |
edges_bfs(g, seed) |
[uv] |
edges_bfs(g, seed, evf) |
[uv, val] |
Iterator category: input (single-pass).
Example:
// BFS from vertex 0
for (auto [v] : views::vertices_bfs(g, 0)) {
std::cout << "BFS level order: " << vertex_id(g, v) << "\n";
}
// BFS edges
for (auto [uv] : views::edges_bfs(g, 0)) {
std::cout << "BFS edge: " << source_id(g, uv) << " -> " << target_id(g, uv) << "\n";
}View accessors:
depth()— maximum depth reached so farnum_visited()— vertices visited so farcancel(cancel_search::cancel_all)— stop iteration immediatelycancel(cancel_search::cancel_branch)— skip current branch
Topological sort traversal for directed acyclic graphs (DAGs).
Signature:
auto vertices_topological_sort(G&& g);
auto vertices_topological_sort(G&& g, VVF&& vvf);
auto vertices_topological_sort(G&& g, VVF&& vvf, Alloc alloc);
auto edges_topological_sort(G&& g);
auto edges_topological_sort(G&& g, EVF&& evf);
auto edges_topological_sort(G&& g, EVF&& evf, Alloc alloc);
// Safe variants with cycle detection
auto vertices_topological_sort_safe(G&& g); // returns tl::expected<view, vertex_t>
auto edges_topological_sort_safe(G&& g); // returns tl::expected<view, vertex_t>Structured Bindings:
| Variant | Binding |
|---|---|
vertices_topological_sort(g) |
[v] |
vertices_topological_sort(g, vvf) |
[v, val] |
edges_topological_sort(g) |
[uv] |
edges_topological_sort(g, evf) |
[uv, val] |
Iterator category: forward (multi-pass).
Example:
// Topological order
for (auto [v] : views::vertices_topological_sort(g)) {
std::cout << "Topo order: " << vertex_id(g, v) << "\n";
}View accessors:
num_visited()— vertices visited so farsize()— total vertices (vertex views only)cancel(cancel_search::cancel_all)— stop iteration immediately
Cycle detection — use the _safe variants, which return tl::expected:
auto result = views::vertices_topological_sort_safe(g);
if (result) {
for (auto [v] : *result) {
// Process vertices in topological order
}
} else {
auto cycle_vertex = result.error(); // vertex that closes the back edge
std::cerr << "Graph has a cycle involving vertex " << cycle_vertex << "\n";
}All search views (BFS, DFS, topological sort) accept an Accessor template
parameter that controls which edges are traversed. By default, views use
out_edge_accessor (outgoing edges). Pass in_edge_accessor as the first
explicit template argument to traverse incoming edges instead.
This requires a graph that supports bidirectional edge access.
Header: <graph/views/edge_accessor.hpp>
| Accessor | edges() |
neighbor_id() |
neighbor() |
|---|---|---|---|
out_edge_accessor (default) |
out_edges(g, u) |
target_id(g, e) |
target(g, e) |
in_edge_accessor |
in_edges(g, u) |
source_id(g, e) |
source(g, e) |
Reverse BFS:
#include <graph/views/bfs.hpp>
#include <graph/views/edge_accessor.hpp>
using namespace graph::views;
// Forward BFS (default)
for (auto [v] : vertices_bfs(g, 0)) { ... }
// Reverse BFS — follow incoming edges
for (auto [v] : vertices_bfs<in_edge_accessor>(g, 3)) { ... }
// Reverse BFS with value function
auto vvf = [](const auto& g, auto& v) { return vertex_id(g, v); };
for (auto [v, id] : vertices_bfs<in_edge_accessor>(g, 3, vvf)) { ... }Reverse DFS:
#include <graph/views/dfs.hpp>
for (auto [v] : vertices_dfs<in_edge_accessor>(g, 3)) { ... }
for (auto [uv] : edges_dfs<in_edge_accessor>(g, 3)) { ... }Reverse Topological Sort:
#include <graph/views/topological_sort.hpp>
for (auto [v] : vertices_topological_sort<in_edge_accessor>(g)) { ... }
// Safe variant with cycle detection
auto result = vertices_topological_sort_safe<in_edge_accessor>(g);
if (result) {
for (auto [v] : *result) { ... }
}Key points:
- The Accessor is a zero-cost abstraction — it is stateless and compiles away entirely.
- Existing code is source-compatible — the default
out_edge_accessormatches prior behavior. - All value function and allocator overloads work with both accessors.
All views support both direct call and pipe syntax:
using namespace graph::views;
// Direct call syntax
auto view = vertexlist(g);
auto view = incidence(g, 0);
auto view = vertices_dfs(g, 0);
// Pipe syntax via adaptors
using namespace graph::views::adaptors;
auto view = g | vertexlist();
auto view = g | incidence(0);
auto view = g | vertices_dfs(0);Value functions receive the graph and descriptor as parameters, enabling stateless lambdas
that support full std::views chaining:
Vertex Value Function:
auto vvf = [](const auto& g, auto& v) {
return degree(g, v);
};
for (auto [uid, u, deg] : views::vertexlist(g, vvf)) {
// deg = degree of vertex
}Edge Value Function:
auto evf = [](const auto& g, auto& uv) {
return edge_value(g, uv);
};
for (auto [vid, uv, w] : views::incidence(g, 0, evf)) {
// w = edge weight
}Complex Value Functions:
// Return struct
struct VertexData {
size_t id;
size_t deg;
};
auto vvf = [](const auto& g, auto& v) -> VertexData {
return {vertex_id(g, v), degree(g, v)};
};
for (auto [uid, u, data] : views::vertexlist(g, vvf)) {
std::cout << "Vertex " << data.id
<< " has degree " << data.deg << "\n";
}Graph views integrate with C++20 standard ranges. Because value functions receive the graph
as a parameter (not via capture), lambdas remain stateless and views satisfy std::ranges::view,
enabling full chaining with std::views adaptors.
#include <ranges>
// Take first N vertices
auto first_five = views::vertexlist(g) | std::views::take(5);
// Filter edges
for (auto [vid, uv] : views::incidence(g, 0)
| std::views::filter([](auto info) {
auto [vid, uv] = info;
return vid > 3;
})) {
// Only edges to vertices > 3
}// Value functions with graph parameter enable chaining!
auto vvf = [](const auto& g, auto& v) { return degree(g, v); };
auto view = views::vertexlist(g, vvf)
| std::views::take(2); // ✅ Works!
auto evf = [](const auto& g, auto& uv) { return edge_value(g, uv); };
auto edges = views::incidence(g, 0, evf)
| std::views::take(3); // ✅ Works!Stateless lambdas (empty capture list []) are default constructible and semiregular,
which satisfies std::ranges::view. Since value functions receive the graph as a parameter
rather than capturing it, they remain stateless:
auto vvf = [](const auto& g, auto v) { return vertex_id(g, v); };
static_assert(std::default_initializable<decltype(vvf)>); // ✅
static_assert(std::semiregular<decltype(vvf)>); // ✅See View Chaining for historical context.
Views compute elements on-demand:
// No computation until iteration
auto view = g | vertexlist(); // O(1)
// Computation happens during iteration
for (auto [v] : view) { // O(n) total
// Process vertex
}Basic views reference graph data without copying:
// vertexlist, incidence, neighbors are zero-copy
auto view = g | vertexlist(); // No allocation
// Search views maintain internal state
auto dfs = g | vertices_dfs(0); // Allocates visited tracker| View | Memory | Notes |
|---|---|---|
| vertexlist | O(1) | References graph |
| incidence | O(1) | References graph |
| neighbors | O(1) | References graph |
| edgelist | O(1) | References graph |
| vertices_dfs | O(V) | Visited tracker + stack |
| edges_dfs | O(V) | Visited tracker + stack |
| vertices_bfs | O(V) | Visited tracker + queue |
| edges_bfs | O(V) | Visited tracker + queue |
| vertices_topological_sort | O(V) | Visited tracker + result |
| edges_topological_sort | O(V) | Visited tracker + result |
| in_incidence | O(1) | References graph |
| in_neighbors | O(1) | References graph |
1. Reuse Value Functions:
// Good: define once, reuse (graph passed as parameter)
auto vvf = [](const auto& g, auto& v) { return vertex_id(g, v); };
auto view1 = views::vertexlist(g, vvf);
auto view2 = views::vertices_dfs(g, 0, vvf);
// Avoid: repeated lambda definitions2. Use Structured Bindings:
// Good: structured binding (compiler optimizes)
for (auto [uid, u, val] : views::vertexlist(g, vvf)) { }3. Early Termination:
// Stop iteration when done
for (auto [v] : views::vertices_dfs(g, 0)) {
if (vertex_id(g, v) == target) {
break; // efficient early exit
}
}4. Custom Allocators:
// Use custom allocator for search views
std::pmr::monotonic_buffer_resource pool;
std::pmr::polymorphic_allocator<bool> alloc(&pool);
auto dfs = vertices_dfs(g, 0, void{}, alloc);
// Faster allocation for visited tracker#include <graph/graph.hpp> // Core types, CPOs, and viewsOr include individual view headers:
#include <graph/views/vertexlist.hpp>
#include <graph/views/dfs.hpp>Pipe syntax is available via adaptor objects:
using namespace graph::views::adaptors;
// Pipe syntax
auto view = g | vertexlist();
// Direct call (also valid)
auto view = graph::views::vertexlist(g);Use structured bindings for clarity:
// Good
for (auto [uid, u, val] : views::vertexlist(g, vvf)) {
// Use uid, u, and val directly
}Views work with const graphs:
void process(const Graph& g) {
for (auto [uid, u] : views::vertexlist(g)) {
// Read-only access
}
}Value functions receive the graph as first parameter, keeping lambdas stateless:
Pattern 1: Simple Transformation
auto vvf = [](const auto& g, auto& v) { return vertex_id(g, v); };Pattern 2: Multiple Values
auto vvf = [](const auto& g, auto& v) {
return std::tuple{vertex_id(g, v), degree(g, v)};
};
for (auto [uid, u, id_and_deg] : views::vertexlist(g, vvf)) { }Pattern 3: Computed Properties
auto vvf = [](const auto& g, auto& v) {
size_t id = vertex_id(g, v);
size_t deg = degree(g, v);
return id * 100 + deg; // composite value
};Pattern 4: Chaining with std::views
auto vvf = [](const auto& g, auto& v) { return degree(g, v); };
auto view = views::vertexlist(g, vvf)
| std::views::take(5); // ✅ Full chaining support!Cycle Detection (topological sort):
auto result = views::vertices_topological_sort_safe(g);
if (result) {
for (auto [v] : *result) {
// Process DAG
}
} else {
// result.error() is the vertex closing the back edge
}Search Cancellation:
auto dfs = views::vertices_dfs(g, 0);
for (auto [v] : dfs) {
if (should_stop(v)) {
dfs.cancel(cancel_search::cancel_all); // stop immediately
break;
}
}// Find all neighbors of a vertex
std::vector<size_t> get_neighbors(const auto& g, size_t vid) {
std::vector<size_t> result;
for (auto [vid, n] : views::neighbors(g, vid)) {
result.push_back(vid);
}
return result;
}// Check if target reachable from source
bool is_reachable(const auto& g, size_t src, size_t tgt) {
for (auto [v] : views::vertices_dfs(g, src)) {
if (vertex_id(g, v) == tgt) {
return true;
}
}
return false;
}// Get topological order as vector
std::vector<size_t> topological_order(const auto& g) {
std::vector<size_t> result;
for (auto [v] : views::vertices_topological_sort(g)) {
result.push_back(vertex_id(g, v));
}
return result;
}// Calculate degree distribution
std::unordered_map<size_t, size_t> degree_distribution(const auto& g) {
std::unordered_map<size_t, size_t> dist;
for (auto [uid, u] : views::vertexlist(g)) {
++dist[degree(g, u)];
}
return dist;
}Basic views (vertexlist, incidence, neighbors, edgelist) and topological sort views
are forward ranges (multi-pass). DFS and BFS views are input ranges (single-pass):
// Forward range — can iterate multiple times
auto vl = views::vertexlist(g);
for (auto [uid, u] : vl) { } // first pass
for (auto [uid, u] : vl) { } // second pass — OK
// Input range — single pass only
auto dfs = views::vertices_dfs(g, 0);
for (auto [v] : dfs) { } // first pass
// Second iteration: create a new view
auto dfs2 = views::vertices_dfs(g, 0);
for (auto [v] : dfs2) { }Descriptors are valid only during iteration:
// Good: store vertex IDs
std::vector<size_t> vertex_ids;
for (auto [uid, u] : views::vertexlist(g)) {
vertex_ids.push_back(uid);
}Don't modify graph structure during iteration:
// Bad: modifying during iteration
for (auto [uid, u] : views::vertexlist(g)) {
g.add_vertex(); // undefined behavior!
}
// Good: collect then modify
std::vector<size_t> to_process;
for (auto [uid, u] : views::vertexlist(g)) {
to_process.push_back(uid);
}
// Now safe to modify graphNote: Modifying property values (
edge_value,vertex_value,graph_value) during iteration is safe — only structural changes (adding/removing vertices or edges) are prohibited.
- Adjacency Lists User Guide — concepts, CPOs, descriptors
- Bidirectional Access — incoming edges, reverse traversal,
in_edge_accessor - Containers User Guide — graph container options
- Edge Lists User Guide — edge list input for graph construction
- CPO Reference — customization point objects
- View Chaining — advanced chaining patterns