diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f2438..199d1f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] ### Added +- **Composable visitor toolkit** (`algorithm/visitor_factory.hpp`, included via `algorithms.hpp` umbrella) — BGL-style event-visitor combinators built on the existing duck-typed `on_*` callback model, without changing any traversal algorithm: + - **Layer 1 — single-event adaptors** (`on_discover_vertex(f)`, `on_tree_edge(f)`, …): wrap a callable so it fires for exactly one traversal event. Generated for all 14 event names (5 vertex + 9 edge) via a macro. Analogous to BGL event tags. + - **Layer 2 — `composite_visitor` / `make_visitor(...)`**: fan a single traversal out to multiple sub-visitors. Each event method is constrained by a fold over the child pack, so a composite exposes `on_X` only when at least one child handles it — keeping `has_on_*` / `valid_visitor` detection accurate and preserving zero-overhead event skipping. Bridges descriptor- and id-form children automatically via `vertex_id()`. + - **Layer 3 — prebuilt recorders**: `predecessor_recorder(pred)`, `distance_recorder(dist, weight_fn)` (weighted), `distance_recorder(dist)` (hop-count), and `time_stamper(time_map, clock)`. Each returns a `(g, x)->void` callable; the caller binds it to the desired event (e.g. `on_tree_edge(predecessor_recorder(pred))` for BFS/DFS or `on_edge_relaxed(predecessor_recorder(pred))` for Dijkstra/Bellman-Ford). + - 9 test cases in `tests/algorithms/test_visitor_factory.cpp` covering compile-time concept checks and runtime BFS/DFS/Dijkstra coverage. +- **`valid_visitor` strict concept** (`algorithm/traversal_common.hpp`) — `has_any_visitor_event` ORs all 19 `has_on_*` concepts; `valid_visitor` is satisfied either by `empty_visitor` or by any type with at least one recognized `on_*` method. A `static_assert(valid_visitor<...>)` is placed at the top of BFS, DFS, Dijkstra, and Bellman-Ford so misspelled event names produce a clear diagnostic instead of a silent no-op. - **`filtered_graph` adaptor** (`adaptors/filtered_graph.hpp`) — non-owning wrapper that filters vertices and edges by predicate during traversal. Satisfies `adjacency_list` so all views and algorithms work transparently on the filtered subgraph. Uses self-contained `filtering_iterator` to avoid `std::views::filter` iterator dangling issues. - **BGL graph adaptor** (`adaptors/bgl/graph_adaptor.hpp`) — adapts Boost.Graph types for use with graph-v3 CPOs, views, and algorithms. Includes `bgl_edge_iterator.hpp` (C++20 iterator wrapper) and `property_bridge.hpp` (BGL property maps → graph-v3 value functions). Supports adjacency_list, CSR, and bidirectional BGL graphs. - `graph::adaptors::keep_all` — sentinel predicate (accepts everything, zero-overhead) diff --git a/agents/bgl_migration_strategy.md b/agents/bgl_migration_strategy.md index d2af443..7000a36 100644 --- a/agents/bgl_migration_strategy.md +++ b/agents/bgl_migration_strategy.md @@ -2,7 +2,7 @@ A comprehensive analysis of the Boost Graph Library (BGL) and graph-v3, identifying gaps, migration paths, and recommended extensions to enable a smooth upgrade transition. -> **Last reviewed:** 2026-05-03 against `include/graph/` source tree. +> **Last reviewed:** 2026-05-30 against `include/graph/` source tree. --- @@ -51,7 +51,6 @@ graph-v3 is a ground-up C++20 redesign targeting ISO standardization (P3126–P3 - No `copy_graph` utility with cross-type and property mapping support - No `labeled_graph` adaptor (string labels → vertex mapping) - No named parameter interface (BGL users must learn new positional API) -- No composable visitor adaptors (`make_visitor(...)` factory) --- @@ -395,7 +394,7 @@ breadth_first_search(g, {s}, my_visitor{}); | **Missing events** | Base class provides no-ops | Simply omit the method — zero overhead | | **Parameter order** | `(vertex, graph)` or `(edge, graph)` | `(graph, vertex_id)` or `(graph, edge)` — graph first | | **Vertex parameter** | `vertex_descriptor` | Both `vertex_id_t` and `vertex_t` overloads supported | -| **Event composition** | `make_bfs_visitor(pair>)` | Not available — write a combined visitor struct | +| **Event composition** | `make_bfs_visitor(pair>)` | `make_visitor(on_tree_edge(predecessor_recorder(pred)), ...)` — see `visitor_factory.hpp` | ### BGL Event → graph-v3 Event Mapping @@ -420,18 +419,58 @@ breadth_first_search(g, {s}, my_visitor{}); **Missing events:** `non_tree_edge`, `gray_target`, `black_target` are BFS-specific events that distinguish edge targets by color state. These could be added to graph-v3's BFS visitor concept if needed for migration. -### Composable Visitor Adaptors — Gap (still open) +### Composable Visitor Adaptors — Implemented -BGL provides reusable event visitor adaptors (`predecessor_recorder`, `distance_recorder`, `time_stamper`, `property_writer`) that can be composed via `std::pair` chaining. graph-v3 has no equivalent — users write monolithic visitor structs. As of 2026-05, no `make_visitor(...)` factory or pre-built event adaptors are available. +BGL provides reusable event visitor adaptors (`predecessor_recorder`, `distance_recorder`, `time_stamper`, `property_writer`) that can be composed via `std::pair` chaining. graph-v3 now provides an equivalent toolkit in `include/graph/algorithm/visitor_factory.hpp` (included by the `graph/algorithms.hpp` umbrella). -**Recommendation:** Consider providing lambda-based visitor construction: +Three layers: + +**Layer 1 — single-event adaptors.** Wrap a callable so it fires for exactly one traversal event, analogous to BGL event tags: +```cpp +on_discover_vertex(f) // vertex events +on_tree_edge(f) // edge events — on_tree_edge, on_back_edge, + // on_edge_relaxed, on_finish_edge, etc. +``` + +**Layer 2 — `make_visitor(...)`.** Fan one traversal out to any number of sub-visitors. Each child may be a single-event adaptor, a prebuilt recorder, or any struct with `on_*` methods. Supports mixing descriptor-form and id-form children in the same call: ```cpp auto vis = make_visitor( - on_discover_vertex([&](auto& g, auto uid) { ... }), - on_examine_edge([&](auto& g, auto uv) { ... }) + on_discover_vertex([&](auto& g, auto uid) { order.push_back(uid); }), + on_tree_edge(predecessor_recorder(pred)) ); +breadth_first_search(g, {s}, vis); +``` + +**Layer 3 — prebuilt recorders.** Ready-made callables for common bookkeeping; the caller binds each to the desired event: + +| Recorder | Description | Typical event binding | +|----------|-------------|----------------------| +| `predecessor_recorder(pred)` | `pred[target_id] = source_id` | `on_tree_edge` (BFS/DFS), `on_edge_relaxed` (Dijkstra) | +| `distance_recorder(dist, weight_fn)` | weighted distance accumulation | `on_edge_relaxed` | +| `distance_recorder(dist)` | hop-count distance | `on_tree_edge` | +| `time_stamper(time_map, clock)` | `time[vertex_id] = clock++` | `on_discover_vertex`, `on_finish_vertex` | + +**BGL vs. graph-v3 comparison:** +```cpp +// BGL: +breadth_first_search(g, s, visitor(make_bfs_visitor(std::make_pair( + record_predecessors(pred.data(), on_tree_edge()), + record_distances(dist.data(), on_tree_edge()))))); + +// graph-v3: +breadth_first_search(g, {s}, + make_visitor( + on_tree_edge(predecessor_recorder(pred)), + on_tree_edge(distance_recorder(dist)), + on_discover_vertex([&](auto&, auto uid){ order.push_back(uid); }))); ``` +Key differences from BGL: +- Recorders are plain `(g, x)->void` callables bound to events by the caller — no event-tag type system. +- Visitors are held by reference, not copied; no `boost::ref` wrapper needed for stateful visitors. +- `composite_visitor` exposes `on_X` only when at least one child handles event `X`, keeping `has_on_*` / `valid_visitor` detection accurate. +- `property_writer` (BGL) has no direct equivalent; use a lambda with `on_examine_vertex` / `on_examine_edge`. + --- ## 8. Graph Adaptors & Views @@ -1055,7 +1094,7 @@ These items block migration for the largest number of BGL users: | **PageRank** | Algorithm | Low | Widely used iterative algorithm | | **DIMACS read/write** | I/O | Low | Required for max-flow benchmark suites | -> **Done since the previous revision of this plan:** `filtered_graph` adaptor, DOT/GraphML/JSON I/O, Erdős-Rényi / Barabási-Albert / 2D grid / path generators, `kosaraju` + `tarjan_scc`, `afforest`, library-shipped BGL adaptor (`include/graph/adaptors/bgl/`). +> **Done since the previous revision of this plan:** `filtered_graph` adaptor, DOT/GraphML/JSON I/O, Erdős-Rényi / Barabási-Albert / 2D grid / path generators, `kosaraju` + `tarjan_scc`, `afforest`, library-shipped BGL adaptor (`include/graph/adaptors/bgl/`), composable visitor toolkit (`visitor_factory.hpp`: `make_visitor`, single-event adaptors, `predecessor_recorder`, `distance_recorder`, `time_stamper`), `valid_visitor` strict concept with `static_assert` diagnostics in BFS/DFS/Dijkstra/Bellman-Ford. ### Phase 2: Common Algorithm Coverage @@ -1085,7 +1124,7 @@ These items block migration for the largest number of BGL users: | **Max Cardinality Matching** | Algorithm | Medium | Bipartite matching | | **Layout algorithms** | Algorithm | Medium | Graph visualization | | **Small World / PLOD generators** | Generator | Low | Synthetic graph generation | -| **Lambda visitor composition** | API | Low | `make_visitor(on_discover_vertex([&](...){...}), ...)` | +| ~~**Lambda visitor composition**~~ | ~~API~~ | ~~Low~~ | ✅ Done — `visitor_factory.hpp`: `make_visitor`, single-event adaptors, `predecessor_recorder`, `distance_recorder`, `time_stamper` | | **BGL compatibility header** | Migration | Medium | `graph_traits` shim + name aliases for gradual migration | ### Phase 4: Ecosystem & Tooling diff --git a/include/graph/algorithm/bellman_ford_shortest_paths.hpp b/include/graph/algorithm/bellman_ford_shortest_paths.hpp index 2d6508e..464c756 100644 --- a/include/graph/algorithm/bellman_ford_shortest_paths.hpp +++ b/include/graph/algorithm/bellman_ford_shortest_paths.hpp @@ -237,6 +237,9 @@ requires distance_fn_for && // Compare&& compare = less>(), Combine&& combine = plus>()) { using graph_type = std::remove_reference_t; + static_assert(valid_visitor, + "Visitor has no recognized on_* callbacks. Check for a misspelled event name " + "(e.g. on_discover_vertx), or pass graph::empty_visitor{} for no callbacks."); using id_type = vertex_id_t; using DistanceValue = distance_fn_value_t; using weight_type = invoke_result_t>; diff --git a/include/graph/algorithm/breadth_first_search.hpp b/include/graph/algorithm/breadth_first_search.hpp index 4099af5..dc0b147 100644 --- a/include/graph/algorithm/breadth_first_search.hpp +++ b/include/graph/algorithm/breadth_first_search.hpp @@ -163,6 +163,9 @@ void breadth_first_search(G&& g, // graph const Sources& sources, Visitor&& visitor = empty_visitor(), const Alloc& alloc = Alloc()) { + static_assert(valid_visitor, + "Visitor has no recognized on_* callbacks. Check for a misspelled event name " + "(e.g. on_discover_vertx), or pass graph::empty_visitor{} for no callbacks."); using id_type = vertex_id_t; // Initialize BFS data structures diff --git a/include/graph/algorithm/depth_first_search.hpp b/include/graph/algorithm/depth_first_search.hpp index 5e7c8b3..7b61f01 100644 --- a/include/graph/algorithm/depth_first_search.hpp +++ b/include/graph/algorithm/depth_first_search.hpp @@ -168,6 +168,9 @@ void depth_first_search(G&& g, // graph const vertex_id_t& start_vertex_id, // starting vertex_id Visitor&& visitor = empty_visitor(), const Alloc& alloc = Alloc()) { + static_assert(valid_visitor, + "Visitor has no recognized on_* callbacks. Check for a misspelled event name " + "(e.g. on_discover_vertx), or pass graph::empty_visitor{} for no callbacks."); using id_type = vertex_id_t; // Vertex color states for DFS diff --git a/include/graph/algorithm/dijkstra_shortest_paths.hpp b/include/graph/algorithm/dijkstra_shortest_paths.hpp index db11f33..04647d6 100644 --- a/include/graph/algorithm/dijkstra_shortest_paths.hpp +++ b/include/graph/algorithm/dijkstra_shortest_paths.hpp @@ -236,6 +236,9 @@ constexpr void dijkstra_shortest_paths( Heap /*heap_tag*/ = Heap{}, const Alloc& alloc = Alloc()) { using graph_type = std::remove_reference_t; + static_assert(valid_visitor, + "Visitor has no recognized on_* callbacks. Check for a misspelled event name " + "(e.g. on_discover_vertx), or pass graph::empty_visitor{} for no callbacks."); using id_type = vertex_id_t; using pred_id_type = predecessor_fn_value_t; using distance_type = distance_fn_value_t; diff --git a/include/graph/algorithm/traversal_common.hpp b/include/graph/algorithm/traversal_common.hpp index 5f5a163..c3d1c10 100644 --- a/include/graph/algorithm/traversal_common.hpp +++ b/include/graph/algorithm/traversal_common.hpp @@ -20,7 +20,9 @@ #pragma once #include +#include #include +#include #include #include #include @@ -327,6 +329,34 @@ concept has_on_finish_edge = requires(Visitor& v, const G& g, const edge_t& e /// Empty visitor type for algorithms that don't require custom callbacks struct empty_visitor {}; +// +// Aggregate / strict visitor concepts +// + +/// Concept satisfied when a visitor handles at least one recognized traversal event. +/// This is the disjunction of every has_on_* visitor concept. It is used by valid_visitor +/// to distinguish a deliberately empty visitor from one whose callbacks were misnamed. +template +concept has_any_visitor_event = // + has_on_initialize_vertex || has_on_initialize_vertex_id || // + has_on_discover_vertex || has_on_discover_vertex_id || // + has_on_examine_vertex || has_on_examine_vertex_id || // + has_on_finish_vertex || has_on_finish_vertex_id || // + has_on_start_vertex || has_on_start_vertex_id || // + has_on_examine_edge || has_on_edge_relaxed || // + has_on_edge_not_relaxed || has_on_edge_minimized || // + has_on_edge_not_minimized || has_on_tree_edge || // + has_on_back_edge || has_on_forward_or_cross_edge || // + has_on_finish_edge; + +/// Strict visitor concept: a type is a valid visitor for graph G if it is the empty_visitor +/// sentinel or it provides at least one recognized on_* callback. This catches the common +/// mistake of misspelling a callback name (e.g. on_discover_vertx), which would otherwise be +/// silently ignored because each event is detected independently via the has_on_* concepts. +template +concept valid_visitor = std::same_as, empty_visitor> || // + has_any_visitor_event; + /** * @brief A null range type for optional predecessor tracking in shortest path algorithms. * diff --git a/include/graph/algorithm/visitor_factory.hpp b/include/graph/algorithm/visitor_factory.hpp new file mode 100644 index 0000000..456a5d8 --- /dev/null +++ b/include/graph/algorithm/visitor_factory.hpp @@ -0,0 +1,259 @@ +/** + * @file visitor_factory.hpp + * @brief Composable visitor utilities for graph traversal algorithms. + * + * This header provides a small, additive toolkit for building traversal visitors out of + * reusable pieces. It is the graph-v3 analogue of Boost.Graph's event-visitor / event-tag + * combinators (e.g. @c make_bfs_visitor, @c predecessor_recorder), expressed with the + * library's duck-typed @c on_* callback convention. + * + * Three layers are provided: + * + * 1. **Single-event adaptors** (`on_discover_vertex(f)`, `on_tree_edge(f)`, ...): wrap a + * callable @c f so it is invoked for exactly one traversal event. These mirror BGL's + * event tags. + * + * 2. **`composite_visitor` / `make_visitor(...)`**: fan a single traversal out to several + * sub-visitors. Each event is forwarded to every child that implements it (by descriptor + * or by vertex id, whichever the child accepts). A composite only exposes an @c on_X + * method when at least one child handles event @c X, so it interoperates with the + * `has_on_*` detection used by the algorithms and with the `valid_visitor` strict check. + * + * 3. **Prebuilt recorders** (`predecessor_recorder`, `distance_recorder`, `time_stamper`): + * ready-made callables for the most common bookkeeping tasks. They return plain + * `(g, x) -> void` callables so the caller chooses the event to bind them to, e.g. + * `on_tree_edge(predecessor_recorder(pred))` for BFS/DFS or + * `on_edge_relaxed(predecessor_recorder(pred))` for Dijkstra/Bellman-Ford. + * + * Nothing here modifies the traversal algorithms; everything is built on the existing + * visitor concepts in @ref traversal_common.hpp. + * + * Used with: breadth_first_search, depth_first_search, dijkstra_shortest_paths, + * bellman_ford_shortest_paths. + * + * --- + * + * **BGL vs. graph-v3 syntax comparison** + * + * BGL (Boost.Graph): + * @code + * // predecessor and distance recorders bound to specific events via event tags, + * // multiplexed with std::make_pair and make_bfs_visitor. + * breadth_first_search(g, s, + * visitor(make_bfs_visitor(std::make_pair( + * record_predecessors(pred.data(), on_tree_edge()), + * record_distances(dist.data(), on_tree_edge()))))); + * @endcode + * + * graph-v3 (this header): + * @code + * // same operations: recorders are callables bound to event adaptors, + * // composited with make_visitor. + * breadth_first_search(g, s, + * make_visitor( + * on_tree_edge(predecessor_recorder(pred)), + * on_tree_edge(distance_recorder(dist)), + * on_discover_vertex([&](auto&, auto u){ order.push_back(vertex_id(g,u)); }))); + * @endcode + * + * Key differences from BGL: + * - No per-algorithm visitor type or base class — any struct with an @c on_* + * method is accepted directly. + * - Visitors are held by reference (not copied); stateful visitors do not + * require @c boost::ref wrapping. + * - Vertex events have both descriptor and vertex-id overloads; the composite + * bridges the two automatically, so a descriptor-only and an id-only child + * can coexist in the same @c make_visitor() call. + * - Unused events compile away to nothing via @c if constexpr, guaranteed + * rather than inliner-dependent. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifndef GRAPH_VISITOR_FACTORY_HPP +# define GRAPH_VISITOR_FACTORY_HPP + +namespace graph { + +// +// Layer 1: single-event adaptors +// +// Each adaptor wraps a callable and exposes exactly one on_* method. The method is a template +// so the same adaptor works whether the algorithm passes a vertex descriptor or a vertex id +// (for vertex events) and regardless of the edge type (for edge events). The wrapped callable +// is invoked as f(g, x). +// +// GRAPH_VISITOR_EVENT_ADAPTOR(on_discover_vertex) generates: +// - struct on_discover_vertex_fn — the adaptor type +// - on_discover_vertex(F&&) -> on_discover_vertex_fn> — the factory function + +# define GRAPH_VISITOR_EVENT_ADAPTOR(EVENT) \ + template \ + struct EVENT##_fn { \ + F f; \ + template \ + void EVENT(const G& g, const X& x) { \ + f(g, x); \ + } \ + }; \ + template \ + [[nodiscard]] EVENT##_fn> EVENT(F&& f) { \ + return {std::forward(f)}; \ + } + +// Vertex events +GRAPH_VISITOR_EVENT_ADAPTOR(on_initialize_vertex) +GRAPH_VISITOR_EVENT_ADAPTOR(on_discover_vertex) +GRAPH_VISITOR_EVENT_ADAPTOR(on_examine_vertex) +GRAPH_VISITOR_EVENT_ADAPTOR(on_finish_vertex) +GRAPH_VISITOR_EVENT_ADAPTOR(on_start_vertex) + +// Edge events +GRAPH_VISITOR_EVENT_ADAPTOR(on_examine_edge) +GRAPH_VISITOR_EVENT_ADAPTOR(on_edge_relaxed) +GRAPH_VISITOR_EVENT_ADAPTOR(on_edge_not_relaxed) +GRAPH_VISITOR_EVENT_ADAPTOR(on_edge_minimized) +GRAPH_VISITOR_EVENT_ADAPTOR(on_edge_not_minimized) +GRAPH_VISITOR_EVENT_ADAPTOR(on_tree_edge) +GRAPH_VISITOR_EVENT_ADAPTOR(on_back_edge) +GRAPH_VISITOR_EVENT_ADAPTOR(on_forward_or_cross_edge) +GRAPH_VISITOR_EVENT_ADAPTOR(on_finish_edge) + +# undef GRAPH_VISITOR_EVENT_ADAPTOR + +// +// Layer 2: composite_visitor +// +// Holds a tuple of sub-visitors and fans every event out to each child that implements it. +// +// For a vertex event the composite receives whatever the algorithm passes (a descriptor, +// because the descriptor-form concept is checked first by the algorithms). Each child is +// then called with the form it supports: the descriptor directly, or vertex_id(g, x) for a +// child that only provides the *_id overload. +// +// Each event method is constrained by a fold expression over the child pack so that the +// method exists only when at least one child handles that event. This keeps has_on_* / +// valid_visitor detection accurate and preserves the algorithms' zero-overhead skipping of +// events that no child cares about. + +template +class composite_visitor { + std::tuple visitors_; + +public: + explicit composite_visitor(Vs... vs) : visitors_(std::move(vs)...) {} + +# define GRAPH_COMPOSITE_VERTEX_EVENT(EVENT) \ + template \ + requires((has_##EVENT || has_##EVENT##_id) || ...) \ + void EVENT(const G& g, const X& x) { \ + std::apply( \ + [&](auto&... child) { \ + auto dispatch = [&](auto& c) { \ + using C = std::remove_reference_t; \ + if constexpr (has_##EVENT) \ + c.EVENT(g, x); \ + else if constexpr (has_##EVENT##_id) \ + c.EVENT(g, vertex_id(g, x)); \ + }; \ + (dispatch(child), ...); \ + }, \ + visitors_); \ + } + +# define GRAPH_COMPOSITE_EDGE_EVENT(EVENT) \ + template \ + requires(has_##EVENT || ...) \ + void EVENT(const G& g, const E& e) { \ + std::apply( \ + [&](auto&... child) { \ + auto dispatch = [&](auto& c) { \ + using C = std::remove_reference_t; \ + if constexpr (has_##EVENT) \ + c.EVENT(g, e); \ + }; \ + (dispatch(child), ...); \ + }, \ + visitors_); \ + } + + GRAPH_COMPOSITE_VERTEX_EVENT(on_initialize_vertex) + GRAPH_COMPOSITE_VERTEX_EVENT(on_discover_vertex) + GRAPH_COMPOSITE_VERTEX_EVENT(on_examine_vertex) + GRAPH_COMPOSITE_VERTEX_EVENT(on_finish_vertex) + GRAPH_COMPOSITE_VERTEX_EVENT(on_start_vertex) + + GRAPH_COMPOSITE_EDGE_EVENT(on_examine_edge) + GRAPH_COMPOSITE_EDGE_EVENT(on_edge_relaxed) + GRAPH_COMPOSITE_EDGE_EVENT(on_edge_not_relaxed) + GRAPH_COMPOSITE_EDGE_EVENT(on_edge_minimized) + GRAPH_COMPOSITE_EDGE_EVENT(on_edge_not_minimized) + GRAPH_COMPOSITE_EDGE_EVENT(on_tree_edge) + GRAPH_COMPOSITE_EDGE_EVENT(on_back_edge) + GRAPH_COMPOSITE_EDGE_EVENT(on_forward_or_cross_edge) + GRAPH_COMPOSITE_EDGE_EVENT(on_finish_edge) + +# undef GRAPH_COMPOSITE_VERTEX_EVENT +# undef GRAPH_COMPOSITE_EDGE_EVENT +}; + +/// Combine several sub-visitors into one. Each traversal event is forwarded to every +/// sub-visitor that implements it. Sub-visitors are stored by value (decayed); wrap a +/// stateful visitor you want to observe afterwards with std::ref, or read it back from the +/// returned composite. +template +[[nodiscard]] composite_visitor...> make_visitor(Vs&&... vs) { + return composite_visitor...>(std::forward(vs)...); +} + +// +// Layer 3: prebuilt recorders +// +// Each factory returns a plain (g, x) -> void callable. Bind it to an event with a +// single-event adaptor, e.g. on_tree_edge(predecessor_recorder(pred)). +// + +/// Record the predecessor (parent) of each edge target: pred[target_id(g, uv)] = source_id(g, uv). +/// Bind to on_tree_edge (BFS/DFS) or on_edge_relaxed (Dijkstra/Bellman-Ford). @p pred must be +/// indexable by vertex id (e.g. a vertex_property_map or std::vector). +template +[[nodiscard]] auto predecessor_recorder(PredMap& pred) { + return [&pred](const auto& g, const auto& uv) { pred[target_id(g, uv)] = source_id(g, uv); }; +} + +/// Record a distance for each edge target: dist[target] = dist[source] + weight(g, uv). +/// Bind to on_tree_edge for unweighted layering or on_edge_relaxed for weighted relaxation. +/// @p dist must be indexable by vertex id and pre-seeded for the source vertices. +template +[[nodiscard]] auto distance_recorder(DistMap& dist, WeightFn weight) { + return [&dist, weight](const auto& g, const auto& uv) { + dist[target_id(g, uv)] = dist[source_id(g, uv)] + weight(g, uv); + }; +} + +/// Record a hop-count distance for each edge target: dist[target] = dist[source] + 1. +/// Convenience overload of distance_recorder for unweighted BFS layering. +template +[[nodiscard]] auto distance_recorder(DistMap& dist) { + return [&dist](const auto& g, const auto& uv) { + dist[target_id(g, uv)] = dist[source_id(g, uv)] + 1; + }; +} + +/// Stamp a monotonically increasing time onto each visited vertex: time[vertex_id(g, u)] = clock++. +/// Bind to a vertex event such as on_discover_vertex or on_finish_vertex. @p clock is advanced +/// by reference so multiple stampers can share one clock for interleaved discover/finish times. +template +[[nodiscard]] auto time_stamper(TimeMap& time, Counter& clock) { + return [&time, &clock](const auto& g, const auto& u) { time[vertex_id(g, u)] = clock++; }; +} + +} // namespace graph + +#endif // GRAPH_VISITOR_FACTORY_HPP diff --git a/include/graph/algorithms.hpp b/include/graph/algorithms.hpp index 9b98eed..b120897 100644 --- a/include/graph/algorithms.hpp +++ b/include/graph/algorithms.hpp @@ -23,6 +23,9 @@ #ifndef GRAPH_ALGORITHMS_HPP #define GRAPH_ALGORITHMS_HPP +// Visitor Utilities +#include "algorithm/visitor_factory.hpp" + // Shortest Path Algorithms #include "algorithm/dijkstra_shortest_paths.hpp" #include "algorithm/bellman_ford_shortest_paths.hpp" diff --git a/tests/algorithms/CMakeLists.txt b/tests/algorithms/CMakeLists.txt index 53805be..146108d 100644 --- a/tests/algorithms/CMakeLists.txt +++ b/tests/algorithms/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(test_algorithms test_tarjan_scc.cpp test_indexed_dary_heap.cpp test_dijkstra_indexed_heap.cpp + test_visitor_factory.cpp ) target_link_libraries(test_algorithms diff --git a/tests/algorithms/test_breadth_first_search.cpp b/tests/algorithms/test_breadth_first_search.cpp index e8898c6..12cc5b5 100644 --- a/tests/algorithms/test_breadth_first_search.cpp +++ b/tests/algorithms/test_breadth_first_search.cpp @@ -16,6 +16,32 @@ using namespace graph::test; using namespace graph::test::fixtures; using namespace graph::test::algorithm; +// ============================================================================= +// valid_visitor concept (strict visitor) compile-time checks +// ============================================================================= + +namespace { +// Recognized callback -> valid visitor. +struct GoodVisitor { + template + void on_discover_vertex(const G&, const V&) {} +}; +// Only a misspelled callback -> NOT a valid visitor. +struct TypoVisitor { + template + void on_discover_vertx(const G&, const V&) {} // typo: missing 'e' +}; +} // namespace + +static_assert(valid_visitor, + "empty_visitor must always be a valid visitor"); +static_assert(valid_visitor, + "visitor with a recognized on_* callback must be valid"); +static_assert(!valid_visitor, + "visitor with only a misspelled callback must be rejected"); +static_assert(!has_any_visitor_event, + "misspelled callback must not be detected as an event"); + // ============================================================================= // Helper Types and Utilities // ============================================================================= diff --git a/tests/algorithms/test_visitor_factory.cpp b/tests/algorithms/test_visitor_factory.cpp new file mode 100644 index 0000000..7108e42 --- /dev/null +++ b/tests/algorithms/test_visitor_factory.cpp @@ -0,0 +1,220 @@ +/** + * @file test_visitor_factory.cpp + * @brief Tests for the composable visitor utilities in visitor_factory.hpp + */ + +#include +#include +#include +#include +#include +#include "../common/graph_fixtures.hpp" +#include "../common/algorithm_test_types.hpp" +#include + +using namespace graph; +using namespace graph::test; +using namespace graph::test::fixtures; +using namespace graph::test::algorithm; + +// ============================================================================= +// Compile-time integration with the strict visitor concept +// ============================================================================= + +namespace { +struct CountingChild { + int discovered = 0; + template + void on_discover_vertex(const G&, const V&) { + ++discovered; + } +}; + +// A sub-visitor that consumes the vertex id form of an event. +struct IdChild { + std::vector* out; + template + void on_discover_vertex(const G&, const vertex_id_t& uid) { + out->push_back(static_cast(uid)); + } +}; +} // namespace + +// A single-event adaptor is a valid visitor. +static_assert(valid_visitor, + "single-event adaptor must be a valid visitor"); +// A composite exposing at least one event is a valid visitor. +static_assert(valid_visitor>, + "composite with a handled event must be a valid visitor"); +// The composite must advertise on_discover_vertex when a child handles it. +static_assert(has_on_discover_vertex>, + "composite must expose on_discover_vertex when a child provides it"); +// The composite must NOT advertise events no child handles. +static_assert(!has_on_examine_edge>, + "composite must not expose events no child handles"); + +// ============================================================================= +// Single-event adaptors +// ============================================================================= + +TEST_CASE("on_discover_vertex adaptor fires only on discovery", "[visitor_factory][adaptor]") { + using Graph = vov_void; + auto g = path_graph_4(); + + int discovered = 0; + auto vis = on_discover_vertex([&](const auto&, const auto&) { ++discovered; }); + + breadth_first_search(g, 0u, vis); + REQUIRE(discovered == 4); +} + +TEST_CASE("on_examine_edge adaptor fires only on edge examination", "[visitor_factory][adaptor]") { + using Graph = vov_void; + auto g = path_graph_4(); + + int edge_count = 0; + auto vis = on_examine_edge([&](const auto&, const auto&) { ++edge_count; }); + + breadth_first_search(g, 0u, vis); + REQUIRE(edge_count == 3); // 3 edges in a 4-vertex path +} + +// ============================================================================= +// composite_visitor / make_visitor +// ============================================================================= + +TEST_CASE("make_visitor fans events out to multiple sub-visitors", "[visitor_factory][composite]") { + using Graph = vov_void; + auto g = path_graph_4(); + + int discovered = 0; + int examined = 0; + int edge_count = 0; + + auto vis = make_visitor( // + on_discover_vertex([&](const auto&, const auto&) { ++discovered; }), // + on_examine_vertex([&](const auto&, const auto&) { ++examined; }), // + on_examine_edge([&](const auto&, const auto&) { ++edge_count; })); + + breadth_first_search(g, 0u, vis); + + REQUIRE(discovered == 4); + REQUIRE(examined == 4); + REQUIRE(edge_count == 3); +} + +TEST_CASE("make_visitor forwards one event to several handlers", "[visitor_factory][composite]") { + using Graph = vov_void; + auto g = path_graph_4(); + + int a = 0; + int b = 0; + + auto vis = make_visitor( // + on_discover_vertex([&](const auto&, const auto&) { ++a; }), // + on_discover_vertex([&](const auto&, const auto&) { ++b; })); + + breadth_first_search(g, 0u, vis); + + REQUIRE(a == 4); + REQUIRE(b == 4); +} + +TEST_CASE("composite bridges descriptor and id sub-visitors", "[visitor_factory][composite]") { + using Graph = vov_void; + auto g = path_graph_4(); + + std::vector by_id; + int by_desc = 0; + + // One child wants the vertex id, the other a descriptor; the composite must + // satisfy both from a single event. + auto vis = make_visitor( // + IdChild{&by_id}, // + on_discover_vertex([&](const auto&, const auto&) { ++by_desc; })); + + breadth_first_search(g, 0u, vis); + + REQUIRE(by_desc == 4); + REQUIRE(by_id == std::vector{0, 1, 2, 3}); +} + +// ============================================================================= +// Prebuilt recorders +// ============================================================================= + +TEST_CASE("predecessor_recorder records parents on tree edges (BFS)", "[visitor_factory][recorder]") { + using Graph = vov_void; + auto g = path_graph_4(); + + std::vector> pred(num_vertices(g)); + for (std::size_t i = 0; i < pred.size(); ++i) + pred[i] = static_cast>(i); + + // BFS examines every edge; on a tree/path each examined edge is a discovery edge. + auto vis = on_examine_edge(predecessor_recorder(pred)); + breadth_first_search(g, 0u, vis); + + REQUIRE(pred[1] == 0); + REQUIRE(pred[2] == 1); + REQUIRE(pred[3] == 2); +} + +TEST_CASE("distance_recorder records hop counts (BFS)", "[visitor_factory][recorder]") { + using Graph = vov_void; + auto g = path_graph_4(); + + std::vector dist(num_vertices(g), 0); + + auto vis = on_examine_edge(distance_recorder(dist)); + breadth_first_search(g, 0u, vis); + + REQUIRE(dist[0] == 0); + REQUIRE(dist[1] == 1); + REQUIRE(dist[2] == 2); + REQUIRE(dist[3] == 3); +} + +TEST_CASE("time_stamper assigns discovery order (DFS)", "[visitor_factory][recorder]") { + using Graph = vov_void; + auto g = path_graph_4(); + + std::vector dtime(num_vertices(g), -1); + int clock = 0; + + auto vis = on_discover_vertex(time_stamper(dtime, clock)); + depth_first_search(g, 0u, vis); + + // Linear chain: discovery times are strictly increasing along the path. + REQUIRE(dtime[0] == 0); + REQUIRE(dtime[1] == 1); + REQUIRE(dtime[2] == 2); + REQUIRE(dtime[3] == 3); +} + +TEST_CASE("predecessor_recorder on edge_relaxed reconstructs Dijkstra tree", "[visitor_factory][recorder]") { + using Graph = vov_weighted; + auto g = path_graph_4_weighted(); + + std::vector distance(num_vertices(g)); + std::vector> predecessor(num_vertices(g)); + init_shortest_paths(g, distance, predecessor); + + // Record predecessors independently via the visitor, in parallel with the + // algorithm's own predecessor map. + std::vector> vis_pred(num_vertices(g)); + for (std::size_t i = 0; i < vis_pred.size(); ++i) + vis_pred[i] = static_cast>(i); + + auto vis = on_edge_relaxed(predecessor_recorder(vis_pred)); + + dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distance), + container_value_fn(predecessor), + [](const auto& gr, const auto& uv) { return edge_value(gr, uv); }, + vis); + + REQUIRE(vis_pred[1] == 0); + REQUIRE(vis_pred[2] == 1); + REQUIRE(vis_pred[3] == 2); +}