Skip to content

Latest commit

 

History

History
942 lines (738 loc) · 24.5 KB

File metadata and controls

942 lines (738 loc) · 24.5 KB
graph-v3 logo

Graph Views Documentation

← Back to Documentation Index

Table of Contents

Overview

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

Quick Start

#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";
}

Basic Views

vertexlist

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 function

Structured 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";
}

incidence

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 function

Structured 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";
}

neighbors

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 function

Structured 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";
}

edgelist

Iterates over all edges in the graph (flattened iteration).

Signature:

auto edgelist(G&& g);
auto edgelist(G&& g, EVF&& evf);  // with value function

Structured 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";
}

Incoming Edge Views

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.

in_incidence

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 function

Structured 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);

in_neighbors

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 function

Structured 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);

Simplified Views (basic_)

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

Search views perform graph traversal and yield vertices/edges in traversal order.

vertices_dfs / edges_dfs

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 depth
  • num_visited() — vertices visited so far
  • cancel(cancel_search::cancel_all) — stop iteration immediately
  • cancel(cancel_search::cancel_branch) — skip current subtree

vertices_bfs / edges_bfs

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 far
  • num_visited() — vertices visited so far
  • cancel(cancel_search::cancel_all) — stop iteration immediately
  • cancel(cancel_search::cancel_branch) — skip current branch

vertices_topological_sort / edges_topological_sort

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 far
  • size() — 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";
}

Reverse Search (Accessor Parameter)

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_accessor matches prior behavior.
  • All value function and allocator overloads work with both accessors.

Range Adaptor Syntax

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

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";
}

Chaining with std::views

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.

Without Value Functions

#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
}

With Value Functions (Full Chaining Support)

// 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!

Why This 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.

Performance Considerations

Lazy Evaluation

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
}

Zero-Copy Design

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

Memory Usage

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

Optimization Tips

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 definitions

2. 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

Best Practices

1. Include Headers

#include <graph/graph.hpp>  // Core types, CPOs, and views

Or include individual view headers:

#include <graph/views/vertexlist.hpp>
#include <graph/views/dfs.hpp>

2. Use Pipe Syntax

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);

3. Structured Bindings

Use structured bindings for clarity:

// Good
for (auto [uid, u, val] : views::vertexlist(g, vvf)) {
    // Use uid, u, and val directly
}

4. Const Correctness

Views work with const graphs:

void process(const Graph& g) {
    for (auto [uid, u] : views::vertexlist(g)) {
        // Read-only access
    }
}

5. Value Function Patterns

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!

6. Error Handling

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;
    }
}

Common Patterns

Pattern 1: Neighbor Iteration

// 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;
}

Pattern 2: Reachability Test

// 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;
}

Pattern 3: Topological Ordering

// 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;
}

Pattern 4: Degree Distribution

// 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;
}

Limitations

1. Iterator Categories

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) { }

2. Descriptor Lifetime

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);
}

3. Graph Mutation

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 graph

Note: Modifying property values (edge_value, vertex_value, graph_value) during iteration is safe — only structural changes (adding/removing vertices or edges) are prohibited.