From 22d3d0aa394ae45bd65eab306b01165042c6393c Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Wed, 1 Apr 2026 21:22:24 -0400 Subject: [PATCH 1/4] docs: document index-only vertex descriptor aliases and constraints - vertex-patterns.md: add index_only_vertex/container_backed_vertex to Foundation Concepts table; add note to Storage Concepts about direct_vertex_type covering both container and index-only iterators; add requires notes to inner_value/underlying_value in Member Functions; add new Index-Only Aliases section with usage example - concepts.md: add index_only_vertex and container_backed_vertex to Descriptor Concepts table - archive/descriptor.md: add index-only iterator category to vertex descriptor spec --- docs/archive/descriptor.md | 1 + docs/reference/concepts.md | 2 ++ docs/reference/vertex-patterns.md | 42 +++++++++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/docs/archive/descriptor.md b/docs/archive/descriptor.md index 097875a..d4b9a15 100644 --- a/docs/archive/descriptor.md +++ b/docs/archive/descriptor.md @@ -115,6 +115,7 @@ Edges in the graph can be stored in various ways depending on graph structure: - The type MUST have at least 2 members (accessible via tuple protocol or pair interface) - The first element serves as the vertex ID (key) - This can be checked using `std::tuple_size::value >= 2` or by requiring `.first` and `.second` members + - **Index-only iterator** (e.g., `iota_view::iterator`): Used for graphs with no physical vertex container. Satisfies `index_only_vertex`. `inner_value()` and `underlying_value()` are not available (constrained out by `container_backed_vertex`). The canonical alias `index_iterator` is provided. - MUST have a single member variable that: - MUST be `size_t index` when the iterator is a random access iterator - MUST be the iterator type itself when the iterator is a forward iterator (non-random access) diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md index 5832a61..74cfff9 100644 --- a/docs/reference/concepts.md +++ b/docs/reference/concepts.md @@ -287,6 +287,8 @@ for full documentation. | `direct_vertex_type` | Random-access, index-based vertex | | `keyed_vertex_type` | Forward, key-based vertex | | `vertex_iterator` | Either direct or keyed | +| `index_only_vertex` | Random-access with integral, non-reference value (no physical container) | +| `container_backed_vertex` | `vertex_iterator` that is not index-only (backed by a real container) | | `random_access_vertex_pattern` | Inner value: return whole element | | `pair_value_vertex_pattern` | Inner value: return `.second` | | `whole_value_vertex_pattern` | Inner value: return `*iter` | diff --git a/docs/reference/vertex-patterns.md b/docs/reference/vertex-patterns.md index f20594c..79f1ab6 100644 --- a/docs/reference/vertex-patterns.md +++ b/docs/reference/vertex-patterns.md @@ -27,6 +27,8 @@ are used internally by both the inner value and storage pattern concepts. | `pair_like` | Has `std::tuple_size::value >= 2` and supports `std::get<0>`, `std::get<1>` (tuple protocol) | | `has_first_second` | Has `.first` and `.second` members | | `pair_like_value` | `pair_like \|\| has_first_second` — disjunction of both | +| `index_only_vertex` | Random-access iterator with integral, non-reference value (e.g., `iota_view` iterator) — no physical container | +| `container_backed_vertex` | `vertex_iterator && !index_only_vertex` — iterator into a real container | ```cpp // Used to constrain keyed_vertex_type, pair_value_vertex_pattern, etc. @@ -125,6 +127,11 @@ stored and extracted: | `direct_vertex_type` | Random-access | `std::size_t` (the index) | `container[index]` | | `keyed_vertex_type` | Forward, pair-like value | Key type (`.first`) | `(*iter).first` for ID, `.second` for data | +`direct_vertex_type` includes both container-backed iterators (e.g., +`vector::iterator`) and index-only iterators (`index_iterator`). Use +`index_only_vertex` or `container_backed_vertex` to +distinguish between the two. + ```cpp // Disjunction template @@ -220,8 +227,8 @@ Constrained by `vertex_iterator`. |----------|---------|-------------| | `value()` | `storage_type` | The raw storage: index (`size_t`) or iterator | | `vertex_id()` | `decltype(auto)` | Vertex ID — index by value for direct, `const&` to key for keyed | -| `underlying_value(Container&)` | `decltype(auto)` | Full container element (`container[i]` or `*iter`); const overload provided | -| `inner_value(Container&)` | `decltype(auto)` | Vertex data excluding the key (`.second` for maps, whole value for vectors); const overload provided | +| `underlying_value(Container&)` | `decltype(auto)` | Full container element (`container[i]` or `*iter`); const overload provided. Requires `container_backed_vertex`. | +| `inner_value(Container&)` | `decltype(auto)` | Vertex data excluding the key (`.second` for maps, whole value for vectors); const overload provided. Requires `container_backed_vertex`. | | `operator++()` / `operator++(int)` | `vertex_descriptor&` / `vertex_descriptor` | Pre/post-increment | | `operator<=>` / `operator==` | | Defaulted three-way and equality comparison | @@ -302,6 +309,37 @@ void process_vertices(/* ... */) { --- +## Index-Only Aliases + +For graphs without a physical vertex container (e.g., `compressed_graph`, +implicit graphs), the library provides canonical aliases: + +| Alias | Definition | +|-------|------------| +| `index_iterator` | `std::ranges::iterator_t>` | +| `index_vertex_descriptor` | `vertex_descriptor` | +| `index_vertex_descriptor_view` | `vertex_descriptor_view` | + +These descriptors store only a `size_t` index. `vertex_id()` and `value()` +work normally. `inner_value()` and `underlying_value()` are not available +(constrained out by `container_backed_vertex`). + +```cpp +#include + +// Create descriptor for vertex 42 — no container needed +graph::index_vertex_descriptor vd{42uz}; +assert(vd.vertex_id() == 42); + +// Iterate over a range of vertex IDs +graph::index_vertex_descriptor_view view(std::size_t{0}, std::size_t{100}); +for (auto desc : view) { + // desc.vertex_id() yields 0, 1, ..., 99 +} +``` + +--- + ## See Also - [User Guide: Adjacency Lists](../user-guide/adjacency-lists.md) — tutorial-style guide From e3db11fbec1a2ce098b57a9742ebdd1617de329f Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Wed, 1 Apr 2026 22:58:13 -0400 Subject: [PATCH 2/4] refactor: remove index storage path from edge_descriptor; simplify vertices(g) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit edge_descriptor always stores EdgeIter directly — edges always have physical containers, so the conditional_t, size_t, EdgeIter> dual-storage path was unnecessary overhead. This eliminates 38 if constexpr branches across 6 files (~380 lines removed). compressed_graph: vertices(g) now returns iota_view(0, n), which the vertices CPO wraps automatically via _wrap_if_needed. The empty-graph special case is no longer needed. vertex_descriptor_view: CTAD deduction guides updated to use std::ranges::iterator_t<> so they work with views (e.g. iota_view) that lack Container::iterator/const_iterator nested types. edge_descriptor_view: Fixed forward_list compatibility — removed spurious sized_range constraint from container constructor, changed if to if constexpr so std::ranges::size() is not compiled for non-sized ranges. Tests: Rewrote test_edge_descriptor.cpp and test_descriptor_traits.cpp for iterator-based storage. Updated test_compressed_graph_cpo.cpp (21 call sites replaced). Fixed pre-existing ODR violation in test_source_id_cpo.cpp (namespace rename to avoid CustomEdge collision). Fixed test_edge_value_cpo.cpp custom graph structs to dereference iterators. Fixed unused vertex_data parameters. --- include/graph/adj_list/descriptor_traits.hpp | 4 +- include/graph/adj_list/edge_descriptor.hpp | 602 ++------------ .../graph/adj_list/edge_descriptor_view.hpp | 26 +- .../graph/adj_list/vertex_descriptor_view.hpp | 4 +- include/graph/container/compressed_graph.hpp | 28 +- tests/adj_list/cpo/test_edge_value_cpo.cpp | 9 +- tests/adj_list/cpo/test_source_id_cpo.cpp | 8 +- .../descriptors/test_descriptor_traits.cpp | 4 +- .../descriptors/test_edge_descriptor.cpp | 749 ++---------------- .../test_compressed_graph_cpo.cpp | 44 +- 10 files changed, 158 insertions(+), 1320 deletions(-) diff --git a/include/graph/adj_list/descriptor_traits.hpp b/include/graph/adj_list/descriptor_traits.hpp index 3c07a72..aa8eefd 100644 --- a/include/graph/adj_list/descriptor_traits.hpp +++ b/include/graph/adj_list/descriptor_traits.hpp @@ -298,7 +298,7 @@ struct is_random_access_descriptor> template struct is_random_access_descriptor> - : std::bool_constant> {}; + : std::false_type {}; /** * @brief Helper variable template for is_random_access_descriptor @@ -319,7 +319,7 @@ struct is_iterator_based_descriptor> template struct is_iterator_based_descriptor> - : std::bool_constant> {}; + : std::true_type {}; /** * @brief Helper variable template for is_iterator_based_descriptor diff --git a/include/graph/adj_list/edge_descriptor.hpp b/include/graph/adj_list/edge_descriptor.hpp index 7473f1a..b73844d 100644 --- a/include/graph/adj_list/edge_descriptor.hpp +++ b/include/graph/adj_list/edge_descriptor.hpp @@ -16,7 +16,11 @@ namespace graph::adj_list { * @brief Descriptor for edges in a graph * * Provides a lightweight, type-safe handle to edges stored in various container types. - * Maintains both the edge location and the source vertex descriptor. + * Maintains both the edge location (as an iterator) and the source vertex descriptor. + * + * Unlike vertex_descriptor which supports both index and iterator storage, + * edge_descriptor always stores the edge iterator directly. There is no need for + * an index-only edge path because edges are always backed by a physical container. * * The EdgeDirection tag controls source/target semantics: * - out_edge_tag (default): source_ is the source vertex, target navigates .edges() @@ -40,21 +44,21 @@ class edge_descriptor { /// True when this descriptor wraps an out-edge (default direction). static constexpr bool is_out_edge = std::is_same_v; - // Conditional storage type for edge: size_t for random access, iterator for forward - using edge_storage_type = std::conditional_t, std::size_t, EdgeIter>; + // Edge storage is always the iterator itself + using edge_storage_type = EdgeIter; // Default constructor constexpr edge_descriptor() noexcept requires std::default_initializable && std::default_initializable : edge_storage_{}, source_{} {} - // Constructor from edge storage and source vertex descriptor + // Constructor from edge iterator and source vertex descriptor constexpr edge_descriptor(edge_storage_type edge_val, vertex_desc source) noexcept : edge_storage_(edge_val), source_(source) {} /** - * @brief Get the underlying edge storage value (index or iterator) - * @return The stored edge index (for random access) or iterator (for forward) + * @brief Get the underlying edge iterator + * @return The stored edge iterator */ [[nodiscard]] constexpr edge_storage_type value() const noexcept { return edge_storage_; } @@ -67,33 +71,9 @@ class edge_descriptor { /** * @brief Get the source vertex ID * @return The vertex ID of the source vertex (by value for integral, const& for map keys) - * - * Extracts the ID from the stored source vertex descriptor. - * Returns decltype(auto) to propagate reference semantics from vertex_descriptor::vertex_id(). */ [[nodiscard]] constexpr decltype(auto) source_id() const noexcept { return source_.vertex_id(); } - /** - * @brief Get the target vertex ID from the edge data - * @param vertex_data The vertex/edge data structure passed from the CPO - * @return The target vertex identifier extracted from the edge - * - * Returns decltype(auto) for reference semantics on non-trivial ID types. - * Pair-like .first branches are parenthesized to return const& to the stable - * key rather than copying. std::get<0> and native .target_id() calls already - * return appropriate types. - */ - /** - * @brief Get the source vertex ID by navigating the edge container (in-edge only) - * @param vertex_data The vertex/edge data structure passed from the CPO - * @return The source vertex identifier extracted from the native in-edge - * - * For in-edge descriptors, source_id() (no args) returns the owning vertex ID - * which is actually the target. This overload navigates the in_edges container - * to retrieve the native edge's source_id — the actual source vertex. - * - * Only available when EdgeDirection is in_edge_tag. - */ /** * @brief Get the source vertex ID by navigating the edge container (in-edge only) * @param vertex_data The vertex/edge data structure passed from the CPO @@ -108,30 +88,7 @@ class edge_descriptor { template [[nodiscard]] constexpr auto source_id(const VertexData& vertex_data) const noexcept requires(is_in_edge) { - const auto& edge_container = [&]() -> decltype(auto) { - if constexpr (requires { vertex_data.in_edges(); }) { - return vertex_data.in_edges(); - } else if constexpr (requires { - vertex_data.first; - vertex_data.second; - }) { - if constexpr (requires { vertex_data.second.in_edges(); }) { - return vertex_data.second.in_edges(); - } else { - return vertex_data.second; - } - } else { - return vertex_data; - } - }(); - - const auto& edge_val = [&]() -> decltype(auto) { - if constexpr (std::random_access_iterator) { - return edge_container[edge_storage_]; - } else { - return *edge_storage_; - } - }(); + const auto& edge_val = *edge_storage_; // Extract source_id from the native edge if constexpr (requires { edge_val.second.source_id(); }) { @@ -153,7 +110,6 @@ class edge_descriptor { * @return The target vertex identifier (the owning vertex) * * For in-edges, the owning vertex IS the target — no container navigation needed. - * The vertex_data parameter is accepted for interface consistency with the CPO. * * Only available when EdgeDirection is in_edge_tag. */ @@ -168,68 +124,29 @@ class edge_descriptor { * @param vertex_data The vertex/edge data structure passed from the CPO * @return The target vertex identifier extracted from the edge * - * Navigates the out-edge container (.edges()) to extract the target vertex ID. + * Dereferences the stored edge iterator to extract the target vertex ID. * * Only available when EdgeDirection is out_edge_tag. */ template - [[nodiscard]] constexpr auto target_id(const VertexData& vertex_data) const noexcept + [[nodiscard]] constexpr auto target_id(const VertexData& /*vertex_data*/) const noexcept requires(is_out_edge) { using edge_value_type = typename std::iterator_traits::value_type; - // Extract the actual edge container from vertex_data: - // 1. If vertex_data has .edges() method, use that (for vov-style vertex) - // 2. If vertex_data is pair-like (from map iterator dereference): - // a. If .second has .edges(), use .second.edges() (for map + vector/list edges) - // b. Otherwise use .second directly (for simpler edge storage) - // 3. Otherwise use vertex_data as-is (raw edge container) - const auto& edge_container = [&]() -> decltype(auto) { - if constexpr (requires { vertex_data.edges(); }) { - return vertex_data.edges(); - } else if constexpr (requires { - vertex_data.first; - vertex_data.second; - }) { - // For map-based vertices, .second is the vertex - // Check if the vertex has .edges() method - if constexpr (requires { vertex_data.second.edges(); }) { - return vertex_data.second.edges(); - } else { - return (vertex_data.second); // parenthesized: return const& to the container - } - } else { - return vertex_data; - } - }(); - - // Get the edge value from container (for random access) or iterator (for forward) - const auto& edge_val = [&]() -> decltype(auto) { - if constexpr (std::random_access_iterator) { - return edge_container[edge_storage_]; - } else { - return *edge_storage_; - } - }(); + const auto& edge_val = *edge_storage_; // Extract target ID from edge value based on its type if constexpr (std::integral) { - // Simple type: the value itself is the target ID return edge_val; } else if constexpr (requires { edge_val.second.target_id(); }) { - // Map-based edge container: edge_val is pair - // Access the edge through .second, then call target_id() return edge_val.second.target_id(); } else if constexpr (requires { edge_val.target_id(); }) { - // Edge object with target_id() member (e.g., dynamic_out_edge) return edge_val.target_id(); } else if constexpr (requires { edge_val.first; }) { - // Pair-like: .first is the target ID — parenthesized for reference semantics return (edge_val.first); } else if constexpr (requires { std::get<0>(edge_val); }) { - // Tuple-like: first element is the target ID return std::get<0>(edge_val); } else { - // Fallback: assume the value itself is the target return edge_val; } } @@ -239,52 +156,11 @@ class edge_descriptor { * @param vertex_data The vertex/edge data structure passed from the CPO * @return Reference to the edge data from the container * - * For random access iterators, accesses container[index]. - * For forward/bidirectional iterators, dereferences the stored iterator. + * Dereferences the stored edge iterator. */ template - [[nodiscard]] constexpr decltype(auto) underlying_value(VertexData& vertex_data) const noexcept { - // Extract the actual edge container from vertex_data - // For in-edge descriptors, navigate .in_edges() instead of .edges() - if constexpr (requires { vertex_data.edges(); }) { - auto& edge_container = [&]() -> decltype(auto) { - if constexpr (is_in_edge && requires { vertex_data.in_edges(); }) return (vertex_data.in_edges()); - else return (vertex_data.edges()); - }(); - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } else if constexpr (requires { - vertex_data.first; - vertex_data.second; - }) { - if constexpr (requires { vertex_data.second.edges(); }) { - auto& edge_container = [&]() -> decltype(auto) { - if constexpr (is_in_edge && requires { vertex_data.second.in_edges(); }) return (vertex_data.second.in_edges()); - else return (vertex_data.second.edges()); - }(); - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } else { - auto& edge_container = vertex_data.second; - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } - } else { - if constexpr (std::random_access_iterator) { - return (vertex_data[edge_storage_]); - } else { - return (*edge_storage_); - } - } + [[nodiscard]] constexpr decltype(auto) underlying_value(VertexData& /*vertex_data*/) const noexcept { + return (*edge_storage_); } /** @@ -293,47 +169,8 @@ class edge_descriptor { * @return Const reference to the edge data from the container */ template - [[nodiscard]] constexpr decltype(auto) underlying_value(const VertexData& vertex_data) const noexcept { - // Extract the actual edge container from vertex_data - if constexpr (requires { vertex_data.edges(); }) { - const auto& edge_container = [&]() -> decltype(auto) { - if constexpr (is_in_edge && requires { vertex_data.in_edges(); }) return (vertex_data.in_edges()); - else return (vertex_data.edges()); - }(); - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } else if constexpr (requires { - vertex_data.first; - vertex_data.second; - }) { - if constexpr (requires { vertex_data.second.edges(); }) { - const auto& edge_container = [&]() -> decltype(auto) { - if constexpr (is_in_edge && requires { vertex_data.second.in_edges(); }) return (vertex_data.second.in_edges()); - else return (vertex_data.second.edges()); - }(); - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } else { - const auto& edge_container = vertex_data.second; - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } - } else { - if constexpr (std::random_access_iterator) { - return (vertex_data[edge_storage_]); - } else { - return (*edge_storage_); - } - } + [[nodiscard]] constexpr decltype(auto) underlying_value(const VertexData& /*vertex_data*/) const noexcept { + return (*edge_storage_); } /** @@ -350,191 +187,31 @@ class edge_descriptor { * For tuples with 3+ elements, this creates a tuple of references to elements [1, N). */ template - [[nodiscard]] constexpr decltype(auto) inner_value(VertexData& vertex_data) const noexcept { + [[nodiscard]] constexpr decltype(auto) inner_value(VertexData& /*vertex_data*/) const noexcept { using edge_value_type = typename std::iterator_traits::value_type; - // Extract the actual edge container from vertex_data - // For in-edge descriptors, navigate .in_edges() instead of .edges() - if constexpr (requires { vertex_data.edges(); }) { - auto& edge_container = [&]() -> decltype(auto) { - if constexpr (is_in_edge && requires { vertex_data.in_edges(); }) return (vertex_data.in_edges()); - else return (vertex_data.edges()); - }(); - // Simple type: just the target ID, return it (no separate property) - if constexpr (std::integral) { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } - // Pair-like: return .second (the property part) - else if constexpr (requires { - std::declval().first; - std::declval().second; - }) { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_].second); - } else { - return ((*edge_storage_).second); - } - } - // Tuple-like: return tuple of references to elements [1, N) - else if constexpr (requires { std::tuple_size::value; }) { - constexpr size_t tuple_size = std::tuple_size::value; - - if constexpr (std::random_access_iterator) { - auto& edge_val = edge_container[edge_storage_]; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_val)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_val)); - } else { - // 3+ elements: return a tuple of references to elements [1, N) - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_val)...); - }(std::make_index_sequence{}); - } - } else { - auto& edge_value = *edge_storage_; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_value)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_value)); - } else { - // 3+ elements: return a tuple of references - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_value)...); - }(std::make_index_sequence{}); - } - } - } - // Custom struct/type: return the whole value - else { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } + auto& edge_val = *edge_storage_; + + if constexpr (std::integral) { + return (edge_val); } else if constexpr (requires { - vertex_data.first; - vertex_data.second; - }) { - // Map pair case - .second is the vertex, get its edges - auto& edge_container = vertex_data.second; - // Simple type: just the target ID, return it (no separate property) - if constexpr (std::integral) { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } - // Pair-like: return .second (the property part) - else if constexpr (requires { std::declval().first; std::declval().second; }) { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_].second); - } else { - return ((*edge_storage_).second); - } - } - // Tuple-like: return tuple of references to elements [1, N) - else if constexpr (requires { std::tuple_size::value; }) { - constexpr size_t tuple_size = std::tuple_size::value; - - if constexpr (std::random_access_iterator) { - auto& edge_val = edge_container[edge_storage_]; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_val)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_val)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_val)...); - }(std::make_index_sequence{}); - } - } else { - auto& edge_value = *edge_storage_; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_value)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_value)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_value)...); - }(std::make_index_sequence{}); - } - } - } - // Custom struct/type: return the whole value - else { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } + return (edge_val.second); + } else if constexpr (requires { std::tuple_size::value; }) { + constexpr size_t tuple_size = std::tuple_size::value; + if constexpr (tuple_size == 1) { + return (std::get<0>(edge_val)); + } else if constexpr (tuple_size == 2) { + return (std::get<1>(edge_val)); + } else { + return [&](std::index_sequence) -> decltype(auto) { + return std::forward_as_tuple(std::get(edge_val)...); + }(std::make_index_sequence{}); } } else { - // Raw edge container - no extraction needed - // Simple type: just the target ID - if constexpr (std::integral) { - if constexpr (std::random_access_iterator) { - return (vertex_data[edge_storage_]); - } else { - return (*edge_storage_); - } - } - // Pair-like: return .second (the property part) - else if constexpr (requires { - std::declval().first; - std::declval().second; - }) { - if constexpr (std::random_access_iterator) { - return (vertex_data[edge_storage_].second); - } else { - return ((*edge_storage_).second); - } - } - // Tuple-like: return tuple of references to elements [1, N) - else if constexpr (requires { std::tuple_size::value; }) { - constexpr size_t tuple_size = std::tuple_size::value; - - if constexpr (std::random_access_iterator) { - auto& edge_val = vertex_data[edge_storage_]; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_val)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_val)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_val)...); - }(std::make_index_sequence{}); - } - } else { - auto& edge_value = *edge_storage_; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_value)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_value)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_value)...); - }(std::make_index_sequence{}); - } - } - } - // Custom struct/type: return the whole value - else { - if constexpr (std::random_access_iterator) { - return (vertex_data[edge_storage_]); - } else { - return (*edge_storage_); - } - } + return (edge_val); } } @@ -544,189 +221,31 @@ class edge_descriptor { * @return Const reference to the edge properties */ template - [[nodiscard]] constexpr decltype(auto) inner_value(const VertexData& vertex_data) const noexcept { + [[nodiscard]] constexpr decltype(auto) inner_value(const VertexData& /*vertex_data*/) const noexcept { using edge_value_type = typename std::iterator_traits::value_type; - // Extract the actual edge container from vertex_data - // For in-edge descriptors, navigate .in_edges() instead of .edges() - if constexpr (requires { vertex_data.edges(); }) { - const auto& edge_container = [&]() -> decltype(auto) { - if constexpr (is_in_edge && requires { vertex_data.in_edges(); }) return (vertex_data.in_edges()); - else return (vertex_data.edges()); - }(); - // Simple type: just the target ID, return it (no separate property) - if constexpr (std::integral) { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } - // Pair-like: return .second (the property part) - else if constexpr (requires { - std::declval().first; - std::declval().second; - }) { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_].second); - } else { - return ((*edge_storage_).second); - } - } - // Tuple-like: return tuple of references to elements [1, N) - else if constexpr (requires { std::tuple_size::value; }) { - constexpr size_t tuple_size = std::tuple_size::value; - - if constexpr (std::random_access_iterator) { - const auto& edge_val = edge_container[edge_storage_]; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_val)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_val)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_val)...); - }(std::make_index_sequence{}); - } - } else { - const auto& edge_value = *edge_storage_; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_value)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_value)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_value)...); - }(std::make_index_sequence{}); - } - } - } - // Custom struct/type: return the whole value - else { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } + const auto& edge_val = *edge_storage_; + + if constexpr (std::integral) { + return (edge_val); } else if constexpr (requires { - vertex_data.first; - vertex_data.second; - }) { - // Map pair case - .second is the vertex, get its edges - const auto& edge_container = vertex_data.second; - // Simple type: just the target ID, return it (no separate property) - if constexpr (std::integral) { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } - } - // Pair-like: return .second (the property part) - else if constexpr (requires { std::declval().first; std::declval().second; }) { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_].second); - } else { - return ((*edge_storage_).second); - } - } - // Tuple-like: return tuple of references to elements [1, N) - else if constexpr (requires { std::tuple_size::value; }) { - constexpr size_t tuple_size = std::tuple_size::value; - - if constexpr (std::random_access_iterator) { - const auto& edge_val = edge_container[edge_storage_]; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_val)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_val)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_val)...); - }(std::make_index_sequence{}); - } - } else { - const auto& edge_value = *edge_storage_; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_value)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_value)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_value)...); - }(std::make_index_sequence{}); - } - } - } - // Custom struct/type: return the whole value - else { - if constexpr (std::random_access_iterator) { - return (edge_container[edge_storage_]); - } else { - return (*edge_storage_); - } + return (edge_val.second); + } else if constexpr (requires { std::tuple_size::value; }) { + constexpr size_t tuple_size = std::tuple_size::value; + if constexpr (tuple_size == 1) { + return (std::get<0>(edge_val)); + } else if constexpr (tuple_size == 2) { + return (std::get<1>(edge_val)); + } else { + return [&](std::index_sequence) -> decltype(auto) { + return std::forward_as_tuple(std::get(edge_val)...); + }(std::make_index_sequence{}); } } else { - // Raw edge container - no extraction needed - // Simple type: just the target ID - if constexpr (std::integral) { - if constexpr (std::random_access_iterator) { - return (vertex_data[edge_storage_]); - } else { - return (*edge_storage_); - } - } - // Pair-like: return .second (the property part) - else if constexpr (requires { - std::declval().first; - std::declval().second; - }) { - if constexpr (std::random_access_iterator) { - return (vertex_data[edge_storage_].second); - } else { - return ((*edge_storage_).second); - } - } - // Tuple-like: return tuple of references to elements [1, N) - else if constexpr (requires { std::tuple_size::value; }) { - constexpr size_t tuple_size = std::tuple_size::value; - - if constexpr (std::random_access_iterator) { - const auto& edge_val = vertex_data[edge_storage_]; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_val)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_val)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_val)...); - }(std::make_index_sequence{}); - } - } else { - const auto& edge_value = *edge_storage_; - if constexpr (tuple_size == 1) { - return (std::get<0>(edge_value)); - } else if constexpr (tuple_size == 2) { - return (std::get<1>(edge_value)); - } else { - return [&](std::index_sequence) -> decltype(auto) { - return std::forward_as_tuple(std::get(edge_value)...); - }(std::make_index_sequence{}); - } - } - } - // Custom struct/type: return the whole value - else { - if constexpr (std::random_access_iterator) { - return (vertex_data[edge_storage_]); - } else { - return (*edge_storage_); - } - } + return (edge_val); } } @@ -759,18 +278,9 @@ namespace std { template struct hash> { [[nodiscard]] size_t operator()(const graph::adj_list::edge_descriptor& ed) const noexcept { - // Combine hash of edge storage and source vertex - size_t h1 = [&ed]() { - if constexpr (std::random_access_iterator) { - return std::hash{}(ed.value()); - } else { - // NOTE: For iterator-based edges, this uses the address of the referenced element. - // This assumes the iterator is dereferenceable (not end). Improving this - // requires a stable edge identity beyond the iterator itself. - return std::hash{}(reinterpret_cast(&(*ed.value()))); - } - }(); - + // Hash using the address of the referenced element. + // This assumes the iterator is dereferenceable (not end). + size_t h1 = std::hash{}(reinterpret_cast(&(*ed.value()))); size_t h2 = std::hash>{}(ed.source()); // Combine hashes using a simple mixing function diff --git a/include/graph/adj_list/edge_descriptor_view.hpp b/include/graph/adj_list/edge_descriptor_view.hpp index 07527fb..9d44fec 100644 --- a/include/graph/adj_list/edge_descriptor_view.hpp +++ b/include/graph/adj_list/edge_descriptor_view.hpp @@ -87,11 +87,7 @@ class edge_descriptor_view : public std::ranges::view_interface) { - size_ = end_val - begin_val; - } else { - size_ = static_cast(std::distance(begin_val, end_val)); - } + size_ = static_cast(std::distance(begin_val, end_val)); } /** @@ -105,13 +101,11 @@ class edge_descriptor_view : public std::ranges::view_interface std::convertible_to; } constexpr edge_descriptor_view(Container& container, vertex_desc source) noexcept : source_(source) { - if constexpr (std::random_access_iterator) { - begin_ = 0; - end_ = static_cast(container.size()); - size_ = container.size(); + begin_ = container.begin(); + end_ = container.end(); + if constexpr(std::ranges::sized_range) { + size_ = std::ranges::size(container); } else { - begin_ = container.begin(); - end_ = container.end(); size_ = static_cast(std::distance(begin_, end_)); } } @@ -130,13 +124,11 @@ class edge_descriptor_view : public std::ranges::view_interface std::convertible_to; } constexpr edge_descriptor_view(const Container& container, vertex_desc source) noexcept : source_(source) { - if constexpr (std::random_access_iterator) { - begin_ = 0; - end_ = static_cast(container.size()); - size_ = container.size(); + begin_ = container.begin(); + end_ = container.end(); + if constexpr(std::ranges::sized_range) { + size_ = std::ranges::size(container); } else { - begin_ = container.begin(); - end_ = container.end(); size_ = static_cast(std::distance(begin_, end_)); } } diff --git a/include/graph/adj_list/vertex_descriptor_view.hpp b/include/graph/adj_list/vertex_descriptor_view.hpp index e52ce4c..3d0c132 100644 --- a/include/graph/adj_list/vertex_descriptor_view.hpp +++ b/include/graph/adj_list/vertex_descriptor_view.hpp @@ -148,10 +148,10 @@ class vertex_descriptor_view : public std::ranges::view_interface -vertex_descriptor_view(Container&) -> vertex_descriptor_view; +vertex_descriptor_view(Container&) -> vertex_descriptor_view>; template -vertex_descriptor_view(const Container&) -> vertex_descriptor_view; +vertex_descriptor_view(const Container&) -> vertex_descriptor_view>; /// Vertex descriptor view for index-only graphs (no physical vertex container). using index_vertex_descriptor_view = vertex_descriptor_view; diff --git a/include/graph/container/compressed_graph.hpp b/include/graph/container/compressed_graph.hpp index b9ed8bc..d037014 100644 --- a/include/graph/container/compressed_graph.hpp +++ b/include/graph/container/compressed_graph.hpp @@ -1084,14 +1084,7 @@ class compressed_graph_base template requires std::derived_from, compressed_graph_base> [[nodiscard]] friend constexpr auto vertices(G&& g) noexcept { - // Index-only vertex descriptor: no physical vertex container, just size_t IDs - using vertex_iter_type = index_iterator; - - if (g.empty()) { - return vertex_descriptor_view(static_cast(0), static_cast(0)); - } - - return vertex_descriptor_view(static_cast(0), static_cast(g.size())); + return std::ranges::iota_view(0, g.num_vertices()); } /** @@ -1235,15 +1228,16 @@ class compressed_graph_base // Check bounds if (vid >= g.size()) { // Return empty view - return edge_desc_view(static_cast(0), static_cast(0), source_vd); + auto col_begin = g.col_index_.begin(); + return edge_desc_view(col_begin, col_begin, source_vd); } // Get the edge range for this vertex from row_index_ auto start_idx = static_cast(g.row_index_[vid].index); auto end_idx = static_cast(g.row_index_[vid + 1].index); - // Return view over the edge range - return edge_desc_view(start_idx, end_idx, source_vd); + // Return view over the edge range using iterators + return edge_desc_view(g.col_index_.begin() + static_cast(start_idx), g.col_index_.begin() + static_cast(end_idx), source_vd); } /** @@ -1262,9 +1256,8 @@ class compressed_graph_base template requires std::derived_from, compressed_graph_base> [[nodiscard]] friend constexpr auto target_id(G&& g, const EdgeDesc& uv) noexcept { - // Edge descriptor's value() returns the edge index into col_index_ - auto edge_idx = uv.value(); - return g.col_index_[edge_idx].index; + // Edge descriptor's value() returns an iterator into col_index_ + return uv.value()->index; } /** @@ -1367,11 +1360,8 @@ class compressed_graph_base template requires std::derived_from, compressed_graph_base> && (!std::is_void_v) [[nodiscard]] friend constexpr decltype(auto) edge_value(G&& g, const E& uv) noexcept { - if constexpr (std::is_const_v>) { - return g.edge_value(static_cast(uv.value())); - } else { - return g.edge_value(static_cast(uv.value())); - } + // Edge descriptor's value() is an iterator into col_index_; compute the offset for edge_value lookup + return g.edge_value(static_cast(uv.value() - g.col_index_.begin())); } /** diff --git a/tests/adj_list/cpo/test_edge_value_cpo.cpp b/tests/adj_list/cpo/test_edge_value_cpo.cpp index e51565b..8fe1da1 100644 --- a/tests/adj_list/cpo/test_edge_value_cpo.cpp +++ b/tests/adj_list/cpo/test_edge_value_cpo.cpp @@ -244,8 +244,7 @@ struct GraphWithByValueEdgeReturn { // Returns by value (transformed), not by reference double edge_value(const edge_descriptor>::iterator, std::vector>>::iterator>& uv) const { - auto& ee = data[uv.source().value()]; - return ee[uv.value()].second * 2.0; // Transform: double the weight + return uv.value()->second * 2.0; // Transform: double the weight } auto begin() { return data.begin(); } @@ -289,15 +288,13 @@ struct GraphWithConstEdgeOverloads { // Non-const version returns mutable reference template double& edge_value(EdgeDesc&& uv) { - auto& ee = data[uv.source().value()]; - return ee[uv.value()].second; + return uv.value()->second; } // Const version returns const reference template const double& edge_value(EdgeDesc&& uv) const { - const auto& ee = data[uv.source().value()]; - return ee[uv.value()].second; + return uv.value()->second; } auto begin() { return data.begin(); } diff --git a/tests/adj_list/cpo/test_source_id_cpo.cpp b/tests/adj_list/cpo/test_source_id_cpo.cpp index 1721819..ed7cb20 100644 --- a/tests/adj_list/cpo/test_source_id_cpo.cpp +++ b/tests/adj_list/cpo/test_source_id_cpo.cpp @@ -138,7 +138,7 @@ TEST_CASE("source_id(g,uv) - vector>> multi-property edges", " // Test: Native Edge Member Function // ============================================================================= -namespace native_edge_member_test { +namespace source_id_native_edge_test { // Custom edge type with source_id() member function struct CustomEdge { int source; @@ -154,10 +154,10 @@ struct CustomEdge { struct CustomGraph { std::vector> adjacency_list = {{{0, 1, 1.5}, {0, 2, 2.5}}, {{1, 3, 3.5}}, {}}; }; -} // namespace native_edge_member_test +} // namespace source_id_native_edge_test TEST_CASE("source_id(g,uv) - native edge member function", "[source_id][cpo][member][native]") { - using namespace native_edge_member_test; + using namespace source_id_native_edge_test; CustomGraph g; auto verts = vertices(g.adjacency_list); @@ -185,7 +185,7 @@ TEST_CASE("source_id(g,uv) - native edge member function", "[source_id][cpo][mem } TEST_CASE("source_id(g,uv) - native edge member priority over descriptor", "[source_id][cpo][member][priority]") { - using namespace native_edge_member_test; + using namespace source_id_native_edge_test; // Even though CustomEdge has a .source field that descriptor would extract, // the source_id() member function should take priority diff --git a/tests/adj_list/descriptors/test_descriptor_traits.cpp b/tests/adj_list/descriptors/test_descriptor_traits.cpp index bb18757..e9d72d4 100644 --- a/tests/adj_list/descriptors/test_descriptor_traits.cpp +++ b/tests/adj_list/descriptors/test_descriptor_traits.cpp @@ -183,8 +183,8 @@ TEST_CASE("edge_descriptor_storage_type extracts edge storage type", "[traits][t using ED_Vector = edge_descriptor; using ED_List = edge_descriptor; - SECTION("Random access edge iterator uses size_t storage") { - STATIC_REQUIRE(std::same_as, std::size_t>); + SECTION("Edge iterator always uses iterator storage") { + STATIC_REQUIRE(std::same_as, VectorIter>); } SECTION("Forward edge iterator uses iterator storage") { diff --git a/tests/adj_list/descriptors/test_edge_descriptor.cpp b/tests/adj_list/descriptors/test_edge_descriptor.cpp index 651907a..e1073ce 100644 --- a/tests/adj_list/descriptors/test_edge_descriptor.cpp +++ b/tests/adj_list/descriptors/test_edge_descriptor.cpp @@ -29,60 +29,63 @@ TEST_CASE("edge_descriptor with random access iterator - vector", "[edge_de using VD = vertex_descriptor; using ED = edge_descriptor; + // We need a live container for iterators + std::vector edge_data = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120}; + SECTION("Default construction") { ED ed; - REQUIRE(ed.value() == 0); + // Default-constructed edge_descriptor exists; source is default-constructed REQUIRE(ed.source().value() == 0); } - SECTION("Construction from edge index and source vertex") { + SECTION("Construction from edge iterator and source vertex") { VD source{5}; - ED ed{3, source}; + ED ed{edge_data.begin() + 3, source}; - REQUIRE(ed.value() == 3); + REQUIRE(ed.value() == edge_data.begin() + 3); REQUIRE(ed.source().value() == 5); REQUIRE(ed.source().vertex_id() == 5); } SECTION("Copy semantics") { VD source{10}; - ED ed1{7, source}; + ED ed1{edge_data.begin() + 7, source}; ED ed2 = ed1; - REQUIRE(ed2.value() == 7); + REQUIRE(ed2.value() == edge_data.begin() + 7); REQUIRE(ed2.source().value() == 10); - ED ed3{1, VD{2}}; + ED ed3{edge_data.begin() + 1, VD{2}}; ed3 = ed1; - REQUIRE(ed3.value() == 7); + REQUIRE(ed3.value() == edge_data.begin() + 7); REQUIRE(ed3.source().value() == 10); } SECTION("Move semantics") { VD source{15}; - ED ed1{8, source}; + ED ed1{edge_data.begin() + 8, source}; ED ed2 = std::move(ed1); - REQUIRE(ed2.value() == 8); + REQUIRE(ed2.value() == edge_data.begin() + 8); REQUIRE(ed2.source().value() == 15); } SECTION("Pre-increment advances edge, keeps source") { VD source{5}; - ED ed{3, source}; + ED ed{edge_data.begin() + 3, source}; ++ed; - REQUIRE(ed.value() == 4); + REQUIRE(ed.value() == edge_data.begin() + 4); REQUIRE(ed.source().value() == 5); // Source unchanged } SECTION("Post-increment") { VD source{5}; - ED ed{3, source}; + ED ed{edge_data.begin() + 3, source}; ED old = ed++; - REQUIRE(old.value() == 3); - REQUIRE(ed.value() == 4); + REQUIRE(old.value() == edge_data.begin() + 3); + REQUIRE(ed.value() == edge_data.begin() + 4); REQUIRE(ed.source().value() == 5); } @@ -90,10 +93,10 @@ TEST_CASE("edge_descriptor with random access iterator - vector", "[edge_de VD source1{5}; VD source2{10}; - ED ed1{3, source1}; - ED ed2{7, source1}; - ED ed3{3, source1}; - ED ed4{3, source2}; // Same edge index, different source + ED ed1{edge_data.begin() + 3, source1}; + ED ed2{edge_data.begin() + 7, source1}; + ED ed3{edge_data.begin() + 3, source1}; + ED ed4{edge_data.begin() + 3, source2}; // Same edge position, different source REQUIRE(ed1 == ed3); REQUIRE(ed1 != ed2); @@ -104,22 +107,22 @@ TEST_CASE("edge_descriptor with random access iterator - vector", "[edge_de SECTION("Hash consistency") { VD source{42}; - ED ed1{10, source}; - ED ed2{10, source}; - ED ed3{11, source}; + ED ed1{edge_data.begin() + 10, source}; + ED ed2{edge_data.begin() + 10, source}; + ED ed3{edge_data.begin() + 11, source}; std::hash hasher; REQUIRE(hasher(ed1) == hasher(ed2)); - // Different edge index should produce different hash (usually) + // Different edge position should produce different hash (usually) } SECTION("Use in std::set") { VD source{5}; std::set ed_set; - ed_set.insert(ED{3, source}); - ed_set.insert(ED{1, source}); - ed_set.insert(ED{3, source}); // duplicate + ed_set.insert(ED{edge_data.begin() + 3, source}); + ed_set.insert(ED{edge_data.begin() + 1, source}); + ed_set.insert(ED{edge_data.begin() + 3, source}); // duplicate REQUIRE(ed_set.size() == 2); } @@ -128,11 +131,11 @@ TEST_CASE("edge_descriptor with random access iterator - vector", "[edge_de VD source{5}; std::unordered_map ed_map; - ed_map[ED{1, source}] = "edge1"; - ed_map[ED{2, source}] = "edge2"; + ed_map[ED{edge_data.begin() + 1, source}] = "edge1"; + ed_map[ED{edge_data.begin() + 2, source}] = "edge2"; REQUIRE(ed_map.size() == 2); - REQUIRE(ed_map[ED{1, source}] == "edge1"); + REQUIRE(ed_map[ED{edge_data.begin() + 1, source}] == "edge1"); } } @@ -219,25 +222,26 @@ TEST_CASE("edge_descriptor_view with vector - per-vertex adjacency", "[edge_desc auto it = view.begin(); ED ed0 = *it; - REQUIRE(ed0.value() == 0); + REQUIRE(ed0.value() == edges_from_v5.begin()); REQUIRE(ed0.source().value() == 5); ++it; ED ed1 = *it; - REQUIRE(ed1.value() == 1); + REQUIRE(ed1.value() == edges_from_v5.begin() + 1); REQUIRE(ed1.source().value() == 5); } SECTION("Range-based for loop") { - View view{edges_from_v5, source}; - std::vector collected_indices; + View view{edges_from_v5, source}; + std::size_t count = 0; for (auto ed : view) { - collected_indices.push_back(ed.value()); + REQUIRE(ed.value() == edges_from_v5.begin() + count); REQUIRE(ed.source().value() == 5); // All have same source + ++count; } - REQUIRE(collected_indices == std::vector{0, 1, 2, 3}); + REQUIRE(count == 4); } SECTION("View satisfies forward_range") { @@ -267,11 +271,12 @@ TEST_CASE("edge_descriptor_view with vector - per-vertex adjacency", "[edge_desc auto count = std::ranges::distance(view); REQUIRE(count == 4); - // Find edge at specific index - auto it = std::ranges::find_if(view, [](ED ed) { return ed.value() == 2; }); + // Find edge at specific position + auto target_it = edges_from_v5.begin() + 2; + auto it = std::ranges::find_if(view, [&](ED ed) { return ed.value() == target_it; }); REQUIRE(it != view.end()); - REQUIRE((*it).value() == 2); + REQUIRE((*it).value() == target_it); REQUIRE((*it).source().value() == 5); } } @@ -365,11 +370,11 @@ TEST_CASE("edge_descriptor_view with various edge data types", "[edge_descriptor REQUIRE(view.size() == 4); - int idx = 0; + auto edge_it = edges.begin(); for (auto ed : view) { - REQUIRE(ed.value() == static_cast(idx)); + REQUIRE(ed.value() == edge_it); REQUIRE(ed.source().value() == 100); - idx++; + ++edge_it; } } } @@ -390,667 +395,11 @@ TEST_CASE("Type safety - edge descriptors with different iterators", "[edge_desc static_assert(!std::same_as); SECTION("Cannot accidentally mix descriptor types") { - VD source{5}; - VectorEdgeDesc ed_vec{3, source}; + VD source{5}; + std::vector vec = {10, 20, 30}; + VectorEdgeDesc ed_vec{vec.begin() + 1, source}; // ListEdgeDesc ed_list = ed_vec; // Would not compile SUCCEED("Types are properly distinct"); } } - -// ============================================================================= -// Multiple Sources / Graph Simulation -// ============================================================================= - -TEST_CASE("Multiple edge views for different source vertices", "[edge_descriptor_view][graph_simulation]") { - using VectorIter = std::vector::iterator; - using VD = vertex_descriptor; - using View = edge_descriptor_view::iterator, VectorIter>; - - // Simulate adjacency lists for different vertices - std::vector edges_from_v0 = {1, 2, 3}; - std::vector edges_from_v1 = {2, 3}; - std::vector edges_from_v2 = {3}; - - View view0{edges_from_v0, VD{0}}; - View view1{edges_from_v1, VD{1}}; - View view2{edges_from_v2, VD{2}}; - - SECTION("Each view has correct source") { - REQUIRE(view0.source().value() == 0); - REQUIRE(view1.source().value() == 1); - REQUIRE(view2.source().value() == 2); - } - - SECTION("Each view has correct edge count") { - REQUIRE(view0.size() == 3); - REQUIRE(view1.size() == 2); - REQUIRE(view2.size() == 1); - } - - SECTION("All edges from each view have correct source") { - for (auto ed : view0) { - REQUIRE(ed.source().value() == 0); - } - - for (auto ed : view1) { - REQUIRE(ed.source().value() == 1); - } - - for (auto ed : view2) { - REQUIRE(ed.source().value() == 2); - } - } -} - -// ============================================================================= -// Target ID Extraction Tests -// ============================================================================= - -TEST_CASE("edge_descriptor::target_id() with simple int edges", "[edge_descriptor][target_id]") { - std::vector edges = {1, 2, 3, 4, 5}; - std::vector vertices = {10, 20, 30, 40, 50}; - - using EdgeIter = std::vector::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - VD source{0}; - ED ed{2, source}; // Points to edge at index 2 (value 3) - - REQUIRE(ed.target_id(edges) == 3); -} - -TEST_CASE("edge_descriptor::target_id() with pair edges", "[edge_descriptor][target_id]") { - std::vector> edges = {{1, 1.5}, {2, 2.5}, {3, 3.5}, {4, 4.5}}; - std::vector vertices = {10, 20, 30, 40, 50}; - - using EdgeIter = std::vector>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - VD source{0}; - ED ed{1, source}; // Points to edge at index 1: (2, 2.5) - - REQUIRE(ed.target_id(edges) == 2); // First element of pair -} - -TEST_CASE("edge_descriptor::target_id() with tuple edges", "[edge_descriptor][target_id]") { - std::vector> edges = {{1, 0, 1.0}, {2, 0, 2.0}, {3, 1, 3.0}}; - std::vector vertices = {10, 20, 30, 40}; - - using EdgeIter = std::vector>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - VD source{0}; - ED ed{2, source}; // Points to edge at index 2: (3, 1, 3.0) - - REQUIRE(ed.target_id(edges) == 3); // First element of tuple -} - -TEST_CASE("edge_descriptor::target_id() with forward iterator - list", "[edge_descriptor][target_id]") { - std::list edges = {5, 10, 15, 20}; - std::vector vertices = {100, 200, 300}; // Use vector for vertices (random access) - - using EdgeIter = std::list::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - auto edge_it = edges.begin(); - std::advance(edge_it, 2); // Point to 15 - - VD source{0}; // Random access vertex descriptor uses index - ED ed{edge_it, source}; - - REQUIRE(ed.target_id(edges) == 15); // Dereferences iterator -} - -// ============================================================================= -// Underlying Value Access Tests -// ============================================================================= - -TEST_CASE("edge_descriptor::underlying_value() with simple int edges", "[edge_descriptor][underlying_value]") { - std::vector edges = {10, 20, 30, 40, 50}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("Access underlying edge value") { - VD source{0}; - ED ed{2, source}; - - REQUIRE(ed.underlying_value(edges) == 30); - } - - SECTION("Modify underlying edge value") { - VD source{1}; - ED ed{3, source}; - - ed.underlying_value(edges) = 999; - REQUIRE(edges[3] == 999); - REQUIRE(ed.underlying_value(edges) == 999); - } - - SECTION("Const access") { - const std::vector const_edges = {1, 2, 3}; - VD source{0}; - ED ed{1, source}; - - REQUIRE(ed.underlying_value(const_edges) == 2); - } -} - -TEST_CASE("edge_descriptor::underlying_value() with pair edges", "[edge_descriptor][underlying_value]") { - std::vector> edges = {{10, 1.5}, {20, 2.5}, {30, 3.5}, {40, 4.5}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("Access pair through underlying_value") { - VD source{0}; - ED ed{1, source}; - - const auto& edge_pair = ed.underlying_value(edges); - REQUIRE(edge_pair.first == 20); - REQUIRE(edge_pair.second == 2.5); - } - - SECTION("Modify pair members") { - VD source{1}; - ED ed{2, source}; - - ed.underlying_value(edges).first = 99; - ed.underlying_value(edges).second = 9.9; - - REQUIRE(edges[2].first == 99); - REQUIRE(edges[2].second == 9.9); - } -} - -TEST_CASE("edge_descriptor::underlying_value() with tuple edges", "[edge_descriptor][underlying_value]") { - std::vector> edges = {{1, 10, 1.0}, {2, 20, 2.0}, {3, 30, 3.0}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("Access tuple through underlying_value") { - VD source{0}; - ED ed{1, source}; - - const auto& edge_tuple = ed.underlying_value(edges); - REQUIRE(std::get<0>(edge_tuple) == 2); - REQUIRE(std::get<1>(edge_tuple) == 20); - REQUIRE(std::get<2>(edge_tuple) == 2.0); - } - - SECTION("Modify tuple members") { - VD source{1}; - ED ed{0, source}; - - std::get<0>(ed.underlying_value(edges)) = 99; - std::get<2>(ed.underlying_value(edges)) = 9.9; - - REQUIRE(std::get<0>(edges[0]) == 99); - REQUIRE(std::get<2>(edges[0]) == 9.9); - } -} - -TEST_CASE("edge_descriptor::underlying_value() with custom struct", "[edge_descriptor][underlying_value]") { - struct Edge { - int target; - std::string label; - double weight; - }; - - std::vector edges = {{10, "A", 1.5}, {20, "B", 2.5}, {30, "C", 3.5}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("Access struct members through underlying_value") { - VD source{0}; - ED ed{1, source}; - - const auto& edge = ed.underlying_value(edges); - REQUIRE(edge.target == 20); - REQUIRE(edge.label == "B"); - REQUIRE(edge.weight == 2.5); - } - - SECTION("Modify struct through underlying_value") { - VD source{1}; - ED ed{2, source}; - - ed.underlying_value(edges).label = "Modified"; - ed.underlying_value(edges).weight = 9.9; - - REQUIRE(edges[2].label == "Modified"); - REQUIRE(edges[2].weight == 9.9); - } -} - -TEST_CASE("edge_descriptor::underlying_value() with forward iterator", "[edge_descriptor][underlying_value]") { - std::list> edges = {{10, 1.0}, {20, 2.0}, {30, 3.0}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::list>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("Access through iterator-based descriptor") { - auto edge_it = edges.begin(); - std::advance(edge_it, 1); - - VD source{0}; - ED ed{edge_it, source}; - - const auto& edge_pair = ed.underlying_value(edges); - REQUIRE(edge_pair.first == 20); - REQUIRE(edge_pair.second == 2.0); - } - - SECTION("Modify through iterator-based descriptor") { - auto edge_it = edges.begin(); - - VD source{1}; - ED ed{edge_it, source}; - - ed.underlying_value(edges).second = 99.9; - REQUIRE(edges.begin()->second == 99.9); - } -} - -// ============================================================================= -// Inner Value Access Tests -// ============================================================================= - -TEST_CASE("edge_descriptor::inner_value() with simple int edges", "[edge_descriptor][inner_value]") { - std::vector edges = {10, 20, 30, 40}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("For simple int edges, inner_value returns the int itself") { - VD source{0}; - ED ed{2, source}; - - // Simple int edges: the value is just the target, so inner_value returns it - REQUIRE(ed.inner_value(edges) == 30); - } -} - -TEST_CASE("edge_descriptor::inner_value() with pair edges", "[edge_descriptor][inner_value]") { - std::vector> edges = {{10, 1.5}, {20, 2.5}, {30, 3.5}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("For pairs, inner_value returns .second (the weight/property)") { - VD source{0}; - ED ed{1, source}; - - // Pair: first is target, second is property - REQUIRE(ed.inner_value(edges) == 2.5); - } - - SECTION("Modify through inner_value") { - VD source{1}; - ED ed{0, source}; - - ed.inner_value(edges) = 9.9; - REQUIRE(edges[0].second == 9.9); - REQUIRE(ed.inner_value(edges) == 9.9); - } - - SECTION("Const access") { - const std::vector> const_edges = {{1, 1.1}, {2, 2.2}}; - VD source{0}; - ED ed{1, source}; - - REQUIRE(ed.inner_value(const_edges) == 2.2); - } -} - -TEST_CASE("edge_descriptor::inner_value() with 2-element tuple", "[edge_descriptor][inner_value]") { - std::vector> edges = {{10, 1.5}, {20, 2.5}, {30, 3.5}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("For 2-element tuple, inner_value returns second element") { - VD source{0}; - ED ed{1, source}; - - REQUIRE(ed.inner_value(edges) == 2.5); - } - - SECTION("Modify through inner_value") { - VD source{1}; - ED ed{2, source}; - - ed.inner_value(edges) = 7.7; - REQUIRE(std::get<1>(edges[2]) == 7.7); - } -} - -TEST_CASE("edge_descriptor::inner_value() with 3-element tuple", "[edge_descriptor][inner_value]") { - std::vector> edges = {{10, 1.5, "A"}, {20, 2.5, "B"}, {30, 3.5, "C"}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("For 3+ element tuple, inner_value returns tuple of last N-1 elements") { - VD source{0}; - ED ed{1, source}; - - // Should return tuple of (double, string) - the property parts - auto props = ed.inner_value(edges); - REQUIRE(std::get<0>(props) == 2.5); - REQUIRE(std::get<1>(props) == "B"); - } - - SECTION("Modify through inner_value tuple") { - VD source{1}; - ED ed{0, source}; - - auto props = ed.inner_value(edges); - std::get<0>(props) = 9.9; - std::get<1>(props) = "Modified"; - - REQUIRE(std::get<1>(edges[0]) == 9.9); - REQUIRE(std::get<2>(edges[0]) == "Modified"); - } -} - -TEST_CASE("edge_descriptor::inner_value() with 4-element tuple", "[edge_descriptor][inner_value]") { - std::vector> edges = {{1, 10, 1.5, "label1"}, {2, 20, 2.5, "label2"}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("For 4-element tuple, returns tuple of last 3 elements") { - VD source{0}; - ED ed{0, source}; - - auto props = ed.inner_value(edges); - REQUIRE(std::get<0>(props) == 10); // 2nd element - REQUIRE(std::get<1>(props) == 1.5); // 3rd element - REQUIRE(std::get<2>(props) == "label1"); // 4th element - } -} - -TEST_CASE("edge_descriptor::inner_value() with custom struct", "[edge_descriptor][inner_value]") { - struct Edge { - int target; - double weight; - std::string label; - }; - - std::vector edges = {{10, 1.5, "A"}, {20, 2.5, "B"}, {30, 3.5, "C"}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::vector::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("For custom struct, inner_value returns the whole struct") { - VD source{0}; - ED ed{1, source}; - - // Custom structs: return whole value (user manages property semantics) - const auto& edge = ed.inner_value(edges); - REQUIRE(edge.target == 20); - REQUIRE(edge.weight == 2.5); - REQUIRE(edge.label == "B"); - } - - SECTION("Modify through inner_value") { - VD source{1}; - ED ed{2, source}; - - ed.inner_value(edges).weight = 9.9; - ed.inner_value(edges).label = "Modified"; - - REQUIRE(edges[2].weight == 9.9); - REQUIRE(edges[2].label == "Modified"); - } -} - -TEST_CASE("edge_descriptor::inner_value() with list iterator", "[edge_descriptor][inner_value]") { - std::list> edges = {{10, 1.0}, {20, 2.0}, {30, 3.0}}; - std::vector vertices = {100, 200, 300}; - - using EdgeIter = std::list>::iterator; - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - using ED = edge_descriptor; - - SECTION("inner_value works with forward iterators") { - auto edge_it = edges.begin(); - std::advance(edge_it, 1); - - VD source{0}; - ED ed{edge_it, source}; - - REQUIRE(ed.inner_value(edges) == 2.0); - } -} - -// ============================================================================= -// Const Semantics Tests -// ============================================================================= - -TEST_CASE("edge_descriptor_view - const container with vector", "[edge_descriptor_view][const]") { - const std::vector edges = {10, 20, 30, 40}; - std::vector vertices = {100, 200, 300}; - - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - - VD source{0}; - - // Construct view from const container - edge_descriptor_view view{edges, source}; - - // The view should deduce const_iterator - using ViewType = decltype(view); - using IterType = typename ViewType::edge_desc::edge_iterator_type; - static_assert(std::is_same_v::const_iterator>, - "Should deduce const_iterator for const container"); - - // Iterate and verify we can access values - std::vector target_ids; - for (auto e : view) { - target_ids.push_back(e.target_id(edges)); - REQUIRE(e.source().value() == 0); - } - - REQUIRE(target_ids.size() == 4); - REQUIRE(target_ids[0] == 10); - REQUIRE(target_ids[1] == 20); - REQUIRE(target_ids[2] == 30); - REQUIRE(target_ids[3] == 40); - - // Verify we can call underlying_value with const container - auto e = *view.begin(); - const auto& val = e.underlying_value(edges); - REQUIRE(val == 10); -} - -TEST_CASE("edge_descriptor_view - non-const container with vector", "[edge_descriptor_view][const]") { - std::vector edges = {10, 20, 30, 40}; - std::vector vertices = {100, 200, 300}; - - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - - VD source{0}; - - // Construct view from non-const container - edge_descriptor_view view{edges, source}; - - // The view should deduce non-const iterator - using ViewType = decltype(view); - using IterType = typename ViewType::edge_desc::edge_iterator_type; - static_assert(std::is_same_v::iterator>, "Should deduce iterator for non-const container"); - - // Verify we can modify through the descriptor - auto e = *view.begin(); - e.underlying_value(edges) = 999; - - REQUIRE(edges[0] == 999); -} - -TEST_CASE("edge_descriptor_view - const container with pairs", "[edge_descriptor_view][const]") { - const std::vector> edges = {{10, 1.5}, {20, 2.5}, {30, 3.5}}; - std::vector vertices = {100, 200, 300}; - - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - - VD source{1}; - - // Construct view from const container - edge_descriptor_view view{edges, source}; - - // The view should deduce const_iterator - using ViewType = decltype(view); - using IterType = typename ViewType::edge_desc::edge_iterator_type; - static_assert(std::is_same_v>::const_iterator>, - "Should deduce const_iterator for const container"); - - // Iterate and verify we can access target IDs - std::vector target_ids; - for (auto e : view) { - target_ids.push_back(e.target_id(edges)); - } - - REQUIRE(target_ids.size() == 3); - REQUIRE(target_ids[0] == 10); - REQUIRE(target_ids[1] == 20); - REQUIRE(target_ids[2] == 30); - - // Verify we can call inner_value with const container - auto e = *view.begin(); - const auto& weight = e.inner_value(edges); - REQUIRE(weight == 1.5); -} - -TEST_CASE("edge_descriptor_view - non-const container with pairs", "[edge_descriptor_view][const]") { - std::vector> edges = {{10, 1.5}, {20, 2.5}, {30, 3.5}}; - std::vector vertices = {100, 200, 300}; - - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - - VD source{2}; - - // Construct view from non-const container - edge_descriptor_view view{edges, source}; - - // The view should deduce non-const iterator - using ViewType = decltype(view); - using IterType = typename ViewType::edge_desc::edge_iterator_type; - static_assert(std::is_same_v>::iterator>, - "Should deduce iterator for non-const container"); - - // Verify we can modify through the descriptor - auto e = *view.begin(); - e.inner_value(edges) = 9.9; // Modify the weight (second element of pair) - - REQUIRE(edges[0].second == 9.9); -} - -TEST_CASE("edge_descriptor_view - const vs non-const distinction", "[edge_descriptor_view][const]") { - std::vector mutable_edges = {1, 2, 3}; - const std::vector const_edges = {4, 5, 6}; - std::vector vertices = {100, 200}; - - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - - VD source{0}; - - edge_descriptor_view mutable_view{mutable_edges, source}; - edge_descriptor_view const_view{const_edges, source}; - - // These should be different types - using MutableViewType = decltype(mutable_view); - using ConstViewType = decltype(const_view); - - static_assert(!std::is_same_v, - "Views from const and non-const containers should be different types"); - - // Verify iterator types are different - using MutableIter = typename MutableViewType::edge_desc::edge_iterator_type; - using ConstIter = typename ConstViewType::edge_desc::edge_iterator_type; - - static_assert(std::is_same_v::iterator>); - static_assert(std::is_same_v::const_iterator>); -} - -TEST_CASE("edge_descriptor_view - const with list container", "[edge_descriptor_view][const]") { - const std::list> edges = {{5, 1.1}, {10, 2.2}, {15, 3.3}}; - std::vector vertices = {100, 200, 300}; - - using VertexIter = std::vector::iterator; - using VD = vertex_descriptor; - - VD source{0}; - - // Construct view from const list - edge_descriptor_view view{edges, source}; - - // Should deduce const_iterator for list - using ViewType = decltype(view); - using IterType = typename ViewType::edge_desc::edge_iterator_type; - static_assert(std::is_same_v>::const_iterator>, - "Should deduce const_iterator for const list"); - - // Verify iteration works - int count = 0; - for (auto e : view) { - REQUIRE(e.source().value() == 0); - count++; - } - REQUIRE(count == 3); - - // Verify we can access but not modify - auto e = *view.begin(); - const auto& weight = e.inner_value(edges); - REQUIRE(weight == 1.1); -} diff --git a/tests/container/compressed_graph/test_compressed_graph_cpo.cpp b/tests/container/compressed_graph/test_compressed_graph_cpo.cpp index 6c65239..e9de28d 100644 --- a/tests/container/compressed_graph/test_compressed_graph_cpo.cpp +++ b/tests/container/compressed_graph/test_compressed_graph_cpo.cpp @@ -261,8 +261,8 @@ TEST_CASE("edges(g,u) returns view of edge descriptors", "[edges][api]") { vector targets; vector values; for (auto ed : e) { - targets.push_back(static_cast(g.target_id(static_cast(ed.value())))); - values.push_back(static_cast(g.edge_value(static_cast(ed.value())))); + targets.push_back(static_cast(target_id(g, ed))); + values.push_back(static_cast(edge_value(g, ed))); ++count; } REQUIRE(count == 2); @@ -280,7 +280,7 @@ TEST_CASE("edges(g,u) returns view of edge descriptors", "[edges][api]") { size_t count = 0; vector targets; for (auto ed : e) { - targets.push_back(static_cast(g.target_id(static_cast(ed.value())))); + targets.push_back(static_cast(target_id(g, ed))); ++count; } REQUIRE(count == 1); @@ -317,7 +317,7 @@ TEST_CASE("edges(g,u) with void edge values", "[edges][api]") { vector targets; for (auto ed : e) { - targets.push_back(static_cast(g.target_id(static_cast(ed.value())))); + targets.push_back(static_cast(target_id(g, ed))); } REQUIRE(targets.size() == 3); @@ -346,8 +346,8 @@ TEST_CASE("edges(g,u) with single edge", "[edges][api]") { int targ = -1; int value = -1; for (auto ed : ee) { - targ = static_cast(g.target_id(static_cast(ed.value()))); - value = static_cast(g.edge_value(static_cast(ed.value()))); + targ = static_cast(target_id(g, ed)); + value = static_cast(edge_value(g, ed)); ++count; } @@ -379,9 +379,9 @@ TEST_CASE("edges(g,u) works with STL algorithms", "[edges][api]") { bool found = false; int found_value = -1; for (auto ed : e) { - if (g.target_id(static_cast(ed.value())) == 2) { + if (target_id(g, ed) == 2) { found = true; - found_value = g.edge_value(static_cast(ed.value())); + found_value = edge_value(g, ed); break; } } @@ -392,7 +392,7 @@ TEST_CASE("edges(g,u) works with STL algorithms", "[edges][api]") { SECTION("collect all targets") { vector targets; for (auto ed : e) { - targets.push_back(static_cast(g.target_id(static_cast(ed.value())))); + targets.push_back(static_cast(target_id(g, ed))); } REQUIRE(targets == vector{1, 2, 3, 4}); } @@ -415,9 +415,9 @@ TEST_CASE("edges(g,u) is a lightweight view", "[edges][api]") { // Both views should produce same results vector targets1, targets2; for (auto ed : e1) - targets1.push_back(static_cast(g.target_id(static_cast(ed.value())))); + targets1.push_back(static_cast(target_id(g, ed))); for (auto ed : e2) - targets2.push_back(static_cast(g.target_id(static_cast(ed.value())))); + targets2.push_back(static_cast(target_id(g, ed))); REQUIRE(targets1 == targets2); REQUIRE(targets1.size() == 2); @@ -436,7 +436,7 @@ TEST_CASE("edges(g,u) with string edge values", "[edges][api]") { vector labels; for (auto ed : e) { - labels.push_back(g.edge_value(static_cast(ed.value()))); + labels.push_back(edge_value(g, ed)); } REQUIRE(labels == vector{"edge_a", "edge_b"}); @@ -456,8 +456,8 @@ TEST_CASE("edges(g,u) const correctness", "[edges][api]") { size_t count = 0; for (auto ed : e) { - [[maybe_unused]] auto targ = g.target_id(static_cast(ed.value())); - [[maybe_unused]] auto value = g.edge_value(static_cast(ed.value())); + [[maybe_unused]] auto targ = target_id(g, ed); + [[maybe_unused]] auto value = edge_value(g, ed); ++count; } REQUIRE(count == 2); @@ -482,8 +482,8 @@ TEST_CASE("edges(g,u) with large graph", "[edges][api]") { size_t count = 0; for (auto ed : ee) { - auto targ = g.target_id(static_cast(ed.value())); - auto value = g.edge_value(static_cast(ed.value())); + auto targ = target_id(g, ed); + auto value = edge_value(g, ed); REQUIRE(static_cast(targ) == static_cast(count + 1)); REQUIRE(value == static_cast((count + 1) * 10)); ++count; @@ -505,7 +505,7 @@ TEST_CASE("edges(g,u) with self-loops", "[edges][api]") { vector targets; for (auto ed : e) { - targets.push_back(static_cast(g.target_id(static_cast(ed.value())))); + targets.push_back(static_cast(target_id(g, ed))); } REQUIRE(targets == vector{0, 1}); } @@ -519,7 +519,7 @@ TEST_CASE("edges(g,u) with self-loops", "[edges][api]") { vector targets; for (auto ed : e) { - targets.push_back(static_cast(g.target_id(static_cast(ed.value())))); + targets.push_back(static_cast(target_id(g, ed))); } REQUIRE(targets == vector{1}); } @@ -680,8 +680,8 @@ TEST_CASE("find_vertex(g,uid) can access edges", "[find_vertex][api]") { vector targets; vector values; for (auto ed : e) { - targets.push_back(static_cast(g.target_id(static_cast(ed.value())))); - values.push_back(static_cast(g.edge_value(static_cast(ed.value())))); + targets.push_back(static_cast(target_id(g, ed))); + values.push_back(static_cast(edge_value(g, ed))); } REQUIRE(targets == vector{1, 2}); REQUIRE(values == vector{10, 20}); @@ -913,8 +913,8 @@ TEST_CASE("target_id(g,uv) consistency with direct access", "[target_id][api]") auto e = edges(g, v0); for (auto ed : e) { - auto edge_idx = ed.value(); - REQUIRE(target_id(g, ed) == g.target_id(static_cast(edge_idx))); + // Verify CPO result matches direct iterator dereference + REQUIRE(target_id(g, ed) == ed.value()->index); } } From c0617a14b07346a60ffdbd8bf0f6b5a63763ca61 Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Wed, 1 Apr 2026 23:07:32 -0400 Subject: [PATCH 3/4] docs: update documentation for edge_descriptor iterator-only storage - CHANGELOG: add entries for edge_descriptor simplification, vertices(g) iota_view, CTAD guide update, and edge_descriptor_view forward_list fix - vertex-patterns.md: CTAD guides now show std::ranges::iterator_t<> - adjacency-list-interface.md: edge_descriptor wraps iterator (not index) - edge-value-concepts.md: target_id() always dereferences stored iterator - adjacency-lists.md: fix edge_descriptor template parameter names - archive/descriptor.md: spec updated for iterator-only edge storage - bgl2_comparison_result.md: updated code sample and size comparison - archive/edge_map_analysis.md: updated code sample --- CHANGELOG.md | 4 ++++ agents/bgl2_comparison_result.md | 20 +++++++---------- docs/archive/descriptor.md | 25 ++++++++-------------- docs/archive/edge_map_analysis.md | 11 +++++----- docs/reference/adjacency-list-interface.md | 2 +- docs/reference/edge-value-concepts.md | 9 +++++--- docs/reference/vertex-patterns.md | 8 +++++-- docs/user-guide/adjacency-lists.md | 2 +- 8 files changed, 40 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 820e44d..3ba6e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ - 456 new algorithm tests for sparse graph types (4343 → 4799) ### Changed +- **`edge_descriptor` simplified to iterator-only storage** — removed the `conditional_t` dual-storage path; edges always store the iterator directly since edges always have physical containers. Eliminates 38 `if constexpr` branches across 6 files (~500 lines removed). +- **`compressed_graph::vertices(g)` returns `iota_view`** — simplified to `std::ranges::iota_view(0, num_vertices())`, which the `vertices` CPO wraps automatically via `_wrap_if_needed`. +- **`vertex_descriptor_view` CTAD deduction guides** — updated from `Container::iterator`/`const_iterator` to `std::ranges::iterator_t<>` for compatibility with views like `iota_view`. +- **`edge_descriptor_view` forward_list compatibility** — fixed constructor to use `if constexpr` for `sized_range` check so `std::ranges::size()` is not compiled for non-sized ranges like `forward_list`. - All algorithms relaxed from `index_adjacency_list` to `adjacency_list` - Algorithm internal arrays use `make_vertex_property_map` (vector or unordered_map depending on graph type) - User-facing `Distances`, `Predecessors`, `Weight`, `Component`, `Label` parameters accept vertex property maps diff --git a/agents/bgl2_comparison_result.md b/agents/bgl2_comparison_result.md index 9caed7a..ebe6b1f 100644 --- a/agents/bgl2_comparison_result.md +++ b/agents/bgl2_comparison_result.md @@ -70,29 +70,25 @@ struct vertex_descriptor { **Edge Descriptor:** ```cpp -// Edge descriptor with conditional storage + source vertex +// Edge descriptor always stores the edge iterator directly template struct edge_descriptor { - // Stores size_t for random access edge containers, iterator otherwise - using edge_storage_type = std::conditional_t< - std::random_access_iterator, - std::size_t, - EdgeIter - >; + // Always stores edge iterator — edges always have physical containers + using edge_storage_type = EdgeIter; edge_storage_type edge_storage_; vertex_descriptor source_; // Source vertex descriptor auto source_id() const; // Delegates to source_.vertex_id() - auto target_id(const VertexData&) const; // Extracts from edge data - auto edge_value(const VertexData&) const; // Edge property access + auto target_id() const; // Extracts from *edge_storage_ + auto edge_value() const; // Edge property access via *edge_storage_ }; ``` **Edge Descriptor Characteristics:** - **Carries source**: Unlike BGL2 which stores source in edge data, graph-v3 stores source vertex descriptor directly -- **Conditional edge storage**: Index for random-access edge containers (vector), iterator for others (forward_list, list) +- **Always iterator-based**: Edge storage is always the edge iterator (unlike vertex descriptors which may be index-only) - **Flexible target extraction**: Supports simple integral targets, pair-like edges, and custom edge types -- **Size**: 16 bytes minimum (8 for edge + 8 for source) for vector-based; larger for iterator-based +- **Size**: Iterator-dependent; typically 8 bytes per iterator + 8 bytes for source vertex index ### Comparative Analysis @@ -117,7 +113,7 @@ struct edge_descriptor { | **Source storage** | In edge data (`source` field) | Embedded `vertex_descriptor` member | | **Target storage** | In edge data (`target` field) | Extracted via `target_id(vertex_data)` | | **Parallel edge support** | `edge_index` field distinguishes duplicates | Iterator position distinguishes duplicates | -| **Size (vector-based)** | 24 bytes (source + target + index) | 16 bytes (edge index + source vertex index) | +| **Size (vector-based)** | 24 bytes (source + target + index) | Iterator-dependent (typically 16 bytes: 8 for edge iterator + 8 for source vertex index) | | **Size (iterator-based)** | Varies by iterator size (for listS/setS) | Varies by iterator size | | **Property access** | `g[e].property` with property map | CPO `edge_value(g, uv)` or `inner_value()` | diff --git a/docs/archive/descriptor.md b/docs/archive/descriptor.md index d4b9a15..677f961 100644 --- a/docs/archive/descriptor.md +++ b/docs/archive/descriptor.md @@ -140,16 +140,11 @@ Edges in the graph can be stored in various ways depending on graph structure: - First parameter: the underlying edge container's iterator type - Second parameter: the vertex iterator type - MUST have two member variables: - - First member: MUST be `size_t index` (for edge index) when the edge iterator is a random access iterator, or the edge iterator type itself when the edge iterator is a forward/bidirectional iterator (non-random access) + - First member: MUST be the edge iterator type (`EdgeIter`). Unlike vertex descriptors which may use `size_t` for index-only graphs, edge descriptors always store the iterator directly since edges always have physical containers. - Second member: MUST be a `vertex_descriptor` (instantiated with the vertex iterator type from the second template parameter) representing the source vertex -- The edge descriptor identifies both WHERE the edge is stored (via first member) and WHICH vertex it originates from (via second member) -- MUST provide a public `value()` member function that returns the underlying edge storage handle: - - When the edge iterator is random access: `value()` MUST return the stored `size_t` index - - When the edge iterator is forward/bidirectional: `value()` MUST return the stored edge iterator - - Return type SHOULD be the exact type of the underlying first member (copy by value) -- MUST provide pre-increment and post-increment operators whose behavior mirrors the underlying edge storage: - - For random access iterators: increment operations MUST advance the `size_t` index by one while leaving the source `vertex_descriptor` unchanged - - For forward/bidirectional iterators: increment operations MUST advance the stored edge iterator while leaving the source `vertex_descriptor` unchanged +- The edge descriptor identifies both WHERE the edge is stored (via iterator) and WHICH vertex it originates from (via source vertex descriptor) +- MUST provide a public `value()` member function that returns the stored edge iterator +- MUST provide pre-increment and post-increment operators that advance the stored edge iterator while leaving the source `vertex_descriptor` unchanged - Directed/undirected semantics are determined by the graph structure, not by the descriptor itself - MUST be efficiently passable by value - MUST integrate with std::hash for unordered containers @@ -164,8 +159,7 @@ Both `vertex_descriptor` and `edge_descriptor` MUST provide specialized function - Random access containers (vector): returns reference to the element at index - Bidirectional containers (map): returns reference to the pair - **For edge_descriptor**: - - Random access containers: returns reference to the edge element at index - - Forward/bidirectional containers: returns reference to the edge element via iterator dereference + - Returns reference to the edge element via iterator dereference (always iterator-based) - **Return type**: MUST use `decltype(auto)` to preserve cv-qualifiers and reference category - **Signature**: `template decltype(auto) underlying_value(Container& container) const noexcept` - **Const overload**: MUST provide const version accepting `const Container&` @@ -277,11 +271,10 @@ This applies to ALL return statements in value-access functions: ### Phase 2: Edge Descriptors 1. Implement edge descriptor template with: - Two template parameters (edge container iterator and vertex iterator) - - First member variable: conditional based on edge iterator category (size_t for random access, iterator for forward/bidirectional) + - First member variable: the edge iterator (`EdgeIter`) — always stores the iterator directly - Second member variable: vertex_descriptor instantiated with the vertex iterator type (represents source vertex) - - Proper std::random_access_iterator and std::forward_iterator concept constraints - - Pre/post increment operators consistent with underlying storage semantics - - `value()` member function returning the underlying edge index or iterator + - Pre/post increment operators that advance the edge iterator + - `value()` member function returning the stored edge iterator - `source()` member function returning the source vertex_descriptor - `target_id()` member function that extracts target vertex ID from edge data (handles int, pair.first, tuple[0], custom types) 2. Implement `edge_descriptor_view` that adapts both per-vertex adjacency storage and global edge storage while yielding descriptors, modelling a forward-only view, and (SHOULD) deriving from `std::ranges::view_interface` @@ -607,7 +600,7 @@ decltype(auto) inner_value(const Container& container) const noexcept; - ✅ Phase 1: Vertex descriptors with conditional storage (index for random access, iterator for bidirectional) - ✅ Phase 1: Vertex descriptor views with forward-only iteration - ✅ Phase 1: Comprehensive vertex descriptor tests (all passing) -- ✅ Phase 2: Edge descriptors with source tracking and conditional storage +- ✅ Phase 2: Edge descriptors with source tracking (always iterator-based storage) - ✅ Phase 2: Edge descriptor views with forward-only iteration - ✅ Phase 2: `target_id()` function for extracting target vertex IDs from edge data - ✅ Phase 2: Comprehensive edge descriptor tests (all passing) diff --git a/docs/archive/edge_map_analysis.md b/docs/archive/edge_map_analysis.md index b2165e0..335505c 100644 --- a/docs/archive/edge_map_analysis.md +++ b/docs/archive/edge_map_analysis.md @@ -144,15 +144,14 @@ The `edge_descriptor` already uses generic iterators: ```cpp template class edge_descriptor { - using edge_storage_type = std::conditional_t< - std::random_access_iterator, - std::size_t, // Store index for vector - EdgeIter // Store iterator for set/map - >; + // Always stores the edge iterator directly + using edge_storage_type = EdgeIter; }; ``` -For `std::map`, `EdgeIter` is a bidirectional iterator (not random access), so the descriptor stores the iterator itself. This already works. +Edge descriptors always store the iterator — unlike vertex descriptors +which may use `size_t` for index-only graphs, edges always have physical +containers. **Target ID Extraction:** diff --git a/docs/reference/adjacency-list-interface.md b/docs/reference/adjacency-list-interface.md index 43ea361..c0849cb 100644 --- a/docs/reference/adjacency-list-interface.md +++ b/docs/reference/adjacency-list-interface.md @@ -205,7 +205,7 @@ over the underlying container's iterator/index representation. | Descriptor | Description | |------------|-------------| | `vertex_descriptor` | Wraps a vertex iterator or index. Provides `vertex_id()` and `inner_value()`. | -| `edge_descriptor` | Wraps an edge iterator or index. Provides `target_id()`, `source_id()`, and `inner_value()`. | +| `edge_descriptor` | Wraps an edge iterator. Provides `target_id()`, `source_id()`, and `inner_value()`. | **Properties:** equality comparison, ordering (if supported), copy/assignment, default construction, `inner_value()` accessor. diff --git a/docs/reference/edge-value-concepts.md b/docs/reference/edge-value-concepts.md index a5dbf7a..049830b 100644 --- a/docs/reference/edge-value-concepts.md +++ b/docs/reference/edge-value-concepts.md @@ -139,11 +139,10 @@ static_assert(edge_pattern_type_v == edge_pattern::custom); The `target_id()` function in `edge_descriptor` uses these patterns to extract the target vertex ID: ```cpp -template -constexpr auto target_id(const EdgeContainer& edges) const noexcept { +constexpr auto target_id() const noexcept { using edge_value_type = typename std::iterator_traits::value_type; - const auto& edge_value = /* get edge from container */; + const auto& edge_value = *edge_storage_; // Always dereference iterator if constexpr (std::integral) { return edge_value; // Simple: value is target ID @@ -160,6 +159,10 @@ constexpr auto target_id(const EdgeContainer& edges) const noexcept { } ``` +`edge_descriptor` always stores the edge iterator directly (never `size_t`), +so `target_id()` simply dereferences the stored iterator — no container +re-navigation needed. + ## Use Cases ### 1. Compile-Time Type Checking diff --git a/docs/reference/vertex-patterns.md b/docs/reference/vertex-patterns.md index 79f1ab6..ddda603 100644 --- a/docs/reference/vertex-patterns.md +++ b/docs/reference/vertex-patterns.md @@ -281,10 +281,14 @@ objects on-the-fly. Inherits from `std::ranges::view_interface`. ### Deduction Guides ```cpp -vertex_descriptor_view(Container&) -> vertex_descriptor_view; -vertex_descriptor_view(const Container&) -> vertex_descriptor_view; +vertex_descriptor_view(Container&) -> vertex_descriptor_view>; +vertex_descriptor_view(const Container&) -> vertex_descriptor_view>; ``` +Using `std::ranges::iterator_t<>` instead of `Container::iterator` allows +deduction to work with views (e.g. `iota_view`) that lack nested +`iterator`/`const_iterator` typedefs. + ### Range Traits `std::ranges::enable_borrowed_range` is specialized to `true` — the view diff --git a/docs/user-guide/adjacency-lists.md b/docs/user-guide/adjacency-lists.md index 466a648..d986bc7 100644 --- a/docs/user-guide/adjacency-lists.md +++ b/docs/user-guide/adjacency-lists.md @@ -309,7 +309,7 @@ Descriptors are value types that identify a single vertex or edge: | Type | Description | |------|-------------| | `vertex_descriptor` | Holds an index or iterator that identifies a vertex; provides `vertex_id()`, `value()`, `inner_value()` | -| `edge_descriptor` | Holds an edge iterator + source vertex descriptor; provides `source_id()`, `target_id()`, `value()` | +| `edge_descriptor` | Holds an edge iterator + source vertex descriptor; provides `source_id()`, `target_id()`, `value()` | Descriptor views are range adaptors that wrap an entire physical range and yield one descriptor per element: From fbf2f13c7aea4d6d5dbbeef14e022afc863febf2 Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Fri, 10 Apr 2026 15:16:10 -0400 Subject: [PATCH 4/4] Update BGL2 comparison query and results with expanded selectors, design features, and algorithm inventory --- agents/bgl2_comparison_query.md | 69 ++- agents/bgl2_comparison_result.md | 928 +++++++++++++++++++------------ 2 files changed, 618 insertions(+), 379 deletions(-) diff --git a/agents/bgl2_comparison_query.md b/agents/bgl2_comparison_query.md index b404186..dc95ce4 100644 --- a/agents/bgl2_comparison_query.md +++ b/agents/bgl2_comparison_query.md @@ -12,24 +12,75 @@ BGL2 now supports 10 container selectors defined in `container_selectors.hpp`: | `vecS` | `std::vector` | O(1) random access, index-based descriptors | | `listS` | `std::list` | O(1) insertion/removal, stable iterator-based descriptors | | `setS` | `std::set` | O(log n), ordered unique elements, stable iterators | -| `mapS` | `std::set` (for edges) | O(log n), key-value storage, prevents parallel edges | +| `mapS` | `std::set` (for edges) | O(log n), prevents parallel edges, stable iterators | | `multisetS` | `std::multiset` | O(log n), ordered with duplicates allowed | | `multimapS` | `std::multiset` (for edges) | O(log n), duplicate keys allowed | -| `hash_setS` | `std::unordered_set` | O(1) average, hash-based unique | -| `hash_mapS` | `std::unordered_set` (for edges) | O(1) average, hash-based | -| `hash_multisetS` | `std::unordered_multiset` | O(1) average, duplicates allowed | -| `hash_multimapS` | `std::unordered_multiset` (for edges) | O(1) average, duplicate keys | +| `hash_setS` | `std::unordered_set` | O(1) average, hash-based unique, **unstable iterators** | +| `hash_mapS` | `std::unordered_set` (for edges) | O(1) average, hash-based, **unstable iterators** | +| `hash_multisetS` | `std::unordered_multiset` | O(1) average, duplicates allowed, **unstable iterators** | +| `hash_multimapS` | `std::unordered_multiset` (for edges) | O(1) average, duplicate keys, **unstable iterators** | + +> **Note on "map" selectors:** Despite having "map" in their names, `mapS`/`multimapS`/`hash_mapS`/`hash_multimapS` all map to **set** containers (not `std::map`). The "map" name reflects the vertex→edge *mapping semantic*, not the container type. The uniqueness/ordering behavior of the underlying set provides the desired parallel-edge control. The `adjacency_list.hpp` provides implementations for: - **Vertex list selectors:** `vecS` (index-based), `listS` (iterator-based), `setS` (iterator-based) - **Out-edge list selectors:** All 10 selectors work for edge containers **Key BGL2 Design Features:** -- Selector concepts: `ContainerSelector`, `SequenceSelector`, `AssociativeSelector`, `UnorderedSelector` -- Selector traits: `is_random_access`, `has_stable_iterators`, `is_ordered`, `is_unique` +- Selector concepts: `ContainerSelector`, `SequenceSelector`, `AssociativeSelector`, `UnorderedSelector`, `StableIteratorSelector`, `RandomAccessSelector` +- Selector traits (per-selector `static constexpr bool`): `is_random_access`, `has_stable_iterators`, `is_ordered`, `is_unique` - `container_gen` maps selectors to container types - `parallel_edge_category_for` determines if parallel edges are allowed (based on `is_unique`) +**BGL2 Additional Graph Containers:** +- `adjacency_matrix.hpp` — dense adjacency matrix representation (fixed vertex count, O(V²) space, O(1) edge lookup) +- `compressed_sparse_row_graph.hpp` — CSR (compressed sparse row) graph (static, cache-efficient) +- `grid_graph.hpp` — N-dimensional implicit grid graph (O(1) space, used in image processing / scientific computing) + +**BGL2 Additional Design Features:** +- `strong_descriptor.hpp` — type-safe `descriptor` wrapper preventing vertex/edge descriptor mix-ups at compile time +- `validation.hpp` — `validate_graph()` framework checking dangling edges, self-loops, parallel edges, bidirectional consistency, etc. +- `algorithm_params.hpp` / `algorithm_result.hpp` — named parameter structs (C++20 designated initializers) and rich result types (e.g., `dijkstra_result` with `path_to()`, `is_reachable()`) +- `visitor_callbacks.hpp` — 9-event BFS/DFS callback structs with `null_callback` defaults and single-event wrappers +- `composable_algorithms.hpp` — range adaptors: `find_components()`, `find_distances_from()`, `find_k_core()`, `find_neighbors()`, `find_common_neighbors()`, `degree_distribution()` +- `parallel_algorithms.hpp` — `parallel_bfs()` (level-synchronous), `parallel_connected_components()` (Shiloach-Vishkin), `parallel_for_each_vertex/edge()` +- `coroutine_traverse.hpp` — lazy coroutine-based BFS/DFS: `bfs_traverse()`, `dfs_traverse()`, multi-source and all-components variants +- `MutableGraph` concept — individual `add_vertex()`, `remove_vertex()`, `add_edge()`, `remove_edge()` + +**Graph-v3 Key Design Features (for comparison context):** +- Concept-based CPO design — any type satisfying graph concepts works with algorithms (no adaptor class needed). A `vector>` or user-defined container works directly. +- Trait-based container configuration (27 trait combinations: `vov`, `vofl`, `mol`, `dov`, etc.) vs BGL2's selector-based approach +- 3 graph containers: `dynamic_graph` (mutable adjacency list), `compressed_graph` (CSR), `undirected_adjacency_list` (dual-list with O(1) edge removal from both endpoints) +- `edge_list` module — standalone edge list support for `pair`, `tuple`, and `edge_data` structs +- Edge descriptors always store the edge iterator directly (no conditional `size_t`/iterator storage) +- Vertex descriptors use `size_t` for index-based graphs, iterator otherwise +- 13 view headers — lazy range adaptors including traversal views (`vertices_dfs()`, `edges_bfs()`, `vertices_topological_sort()`) and pipe syntax (`g | vertexlist() | ...`) +- `dynamic_graph` mutations are batch-oriented (`load_edges()`, `load_vertices()`) — no individual `add_vertex()` / `remove_vertex()` / `add_edge()` / `remove_edge()` +- `vertex_property_map` — `vector` for index graphs, `unordered_map` for mapped graphs, with `container_value_fn()` wrapping for algorithm compatibility +- Visitor-based algorithms: BFS/DFS support `on_discover_vertex`, `on_examine_edge`, `on_tree_edge`, `on_back_edge`, etc. + +**Algorithm Inventory:** + +| Algorithm | BGL2 | Graph-v3 | +|-----------|------|----------| +| BFS | ✅ | ✅ | +| DFS | ✅ (edge classification) | ✅ (edge classification) | +| Dijkstra shortest paths | ✅ | ✅ (multi-source) | +| Bellman-Ford shortest paths | ✅ (negative cycle detection) | ✅ (negative cycle detection) | +| Topological sort | ✅ | ✅ (full-graph, single-source, multi-source) | +| Connected components | ✅ | ✅ (DFS-based + afforest) | +| MST (Kruskal + Prim) | ✅ (separate headers) | ✅ (combined mst.hpp, includes in-place Kruskal) | +| Strongly connected components | ✅ (Tarjan) | ✅ (Kosaraju, 2 overloads: transpose or bidirectional) | +| Triangle counting | — | ✅ (undirected + directed) | +| Articulation points | — | ✅ (iterative Hopcroft-Tarjan) | +| Biconnected components | — | ✅ (Hopcroft-Tarjan with edge stack) | +| Jaccard similarity | — | ✅ (per-edge coefficient) | +| Label propagation | — | ✅ (community detection) | +| Maximum independent set | — | ✅ (greedy) | +| Composable algorithms | ✅ (k-core, degree dist, etc.) | — | +| Coroutine traversal | ✅ (lazy BFS/DFS generators) | — | +| Parallel algorithms | ✅ (parallel BFS, parallel CC) | — | + Compare and contrast BGL2 with the current library (graph-v3), making sure that the following topics are covered: - The differences between the vertex and edge descriptors. - The ability to adapt to pre-existing graph data structures. @@ -37,6 +88,6 @@ Compare and contrast BGL2 with the current library (graph-v3), making sure that - Container flexibility: BGL2's selector-based approach vs graph-v3's trait-based approach. - Other areas of interest in the design and flexibility for the libraries. -Algorithms have not been implemented for graph-v3 and is a known limitation. +Compare and contrast the algorithms implemented in each library. -Output the result to `agents/bgl2_comparison_result.md`. +Replace the contents of `agents/bgl2_comparison_result.md` with the result. diff --git a/agents/bgl2_comparison_result.md b/agents/bgl2_comparison_result.md index ebe6b1f..9e7e333 100644 --- a/agents/bgl2_comparison_result.md +++ b/agents/bgl2_comparison_result.md @@ -1,8 +1,8 @@ # BGL2 vs Graph-v3 Comparative Evaluation -**Date:** December 2024 (Updated with BGL2 container selector improvements) -**BGL2 Location:** `/home/phil/dev_graph/boost/libs/graph/modern/` -**Graph-v3 Location:** `/home/phil/dev_graph/desc/` +**Date:** April 2025 (Updated with edge_descriptor simplification and vertices(g) iota_view changes) +**BGL2 Location:** `/home/phil/dev_graph/boost/libs/graph/modern/` +**Graph-v3 Location:** `/home/phil/dev_graph/graph-v3/` --- @@ -10,120 +10,124 @@ ### BGL2 Approach -BGL2 uses a **traditional, type-centric** approach to descriptors: +BGL2 uses a **selector-dependent descriptor** model where the vertex descriptor type is determined by the vertex list selector: ```cpp -// Vertex descriptor varies by VertexListS selector: -// - vecS: std::size_t (index-based) -// - listS/setS: iterator into the container (iterator-based) -using vertex_descriptor = /* varies by selector */; - -// Edge descriptor is a lightweight struct -struct adjacency_list_edge { - vertex_descriptor source; - vertex_descriptor target; - std::size_t edge_index; // for parallel edge distinction +// vecS: vertex_descriptor = std::size_t (index-based) +// listS: vertex_descriptor = std::list::iterator +// setS: vertex_descriptor = std::set::iterator +``` + +**Edge descriptor** is a lightweight struct carrying source, target, and a global edge index: + +```cpp +template +struct adjacency_list_edge { + VertexDescriptor source; + VertexDescriptor target; + std::size_t edge_index; // Global index into edges_ vector + + bool operator==(const adjacency_list_edge&) const = default; + auto operator<=>(const adjacency_list_edge&) const = default; +}; +``` + +**Additional edge descriptor variants** exist for other graph containers: +- `matrix_edge` — source + target only (adjacency matrix) +- `csr_edge_descriptor` — source_vertex + edge_index + target_vertex (CSR graph) +- `grid_edge` — source/target coordinates + dimension + direction (grid graph) + +**Optional strong typing** via `descriptor` wrapper (`strong_descriptor.hpp`): +```cpp +template +class descriptor { + index_type index_ = null_value; +public: + static constexpr index_type null_value = static_cast(-1); + [[nodiscard]] constexpr bool valid() const noexcept { return index_ != null_value; } + [[nodiscard]] constexpr bool null() const noexcept { return index_ == null_value; } }; +using strong_vertex_descriptor = descriptor; +using strong_edge_index = descriptor; ``` +This prevents mixing vertex and edge descriptors at compile time. **Key Characteristics:** -- **Selector-based vertex descriptors**: `std::size_t` for `vecS`, container iterator for `listS`/`setS` -- **10 container selectors fully implemented**: - - Sequence: `vecS` (vector), `listS` (list) - - Ordered associative: `setS`, `mapS`, `multisetS`, `multimapS` - - Hash-based: `hash_setS`, `hash_mapS`, `hash_multisetS`, `hash_multimapS` -- **Vertex list implementations**: `vecS` (index-based), `listS` (stable iterators), `setS` (ordered, stable) -- **All 10 out-edge selectors**: Work for edge containers -- **Optional strong typing**: `descriptor` wrapper prevents mixing vertex/edge descriptors at compile time -- **Null vertex convention**: `static_cast(-1)` for invalid/null vertices (vecS) -- **Extracted via traits**: `vertex_descriptor_t` uses `graph_traits::vertex_descriptor` -- **Parallel edge control**: `parallel_edge_category_for` auto-detects based on selector's `is_unique` trait +- Selector-based: `vecS` → `size_t`, `listS`/`setS` → iterator +- 10 container selectors available for out-edge lists; 3 for vertex lists (`vecS`, `listS`, `setS`) +- Null vertex convention: `static_cast(-1)` for vecS +- Parallel edge control via `parallel_edge_category_for` (based on selector's `is_unique` trait) +- Extracted via `graph_traits::vertex_descriptor` ### Graph-v3 Approach -Graph-v3 uses a **conditionally-typed descriptor** pattern: +Graph-v3 uses a **template-based descriptor** model with conditional storage for vertices: ```cpp -// Vertex descriptor with conditional storage -template -struct vertex_descriptor { - // Stores size_t for random access containers, iterator otherwise +template +class vertex_descriptor { using storage_type = std::conditional_t< std::random_access_iterator, - std::size_t, // 8 bytes for vector/deque - VertexIter // Iterator for map/list/forward_list - >; + std::size_t, // Index for vector/deque (8 bytes) + VertexIter>; // Iterator for map/list storage_type storage_; - - auto vertex_id() const; // Returns index or extracts key from iterator - auto inner_value(Container& c) const; // Access requires container +public: + auto vertex_id() const; // Returns index or key + auto underlying_value(Container&) const; // Get vertex data + auto inner_value(Container&) const; // Get value (map: .second, vector: whole) }; ``` -**Key Characteristics:** -- **Conditional storage**: Stores `size_t` index for random-access containers (vector, deque), iterator for others (map, list) -- **Lightweight for vectors**: Same 8-byte overhead as BGL2 when using vector-based storage -- **Pattern-aware**: Automatically detects storage patterns (random_access, pair_value, whole_value) -- **Synthesized on iteration**: `vertex_descriptor_view` creates descriptors on-the-fly during traversal -- **Flexible ID types**: Supports non-integral vertex IDs (strings, custom keys) for map-based containers -- **Container access needed**: `inner_value(container)` requires container reference to access vertex data - -**Edge Descriptor:** +For **random-access containers** (vector, deque), `vertices(g)` returns an `iota_view` wrapped in a `vertex_descriptor_view`, so vertex descriptors hold a lightweight `size_t` index. + +**Edge descriptors** always store the edge iterator directly — no conditional storage: + ```cpp -// Edge descriptor always stores the edge iterator directly -template -struct edge_descriptor { - // Always stores edge iterator — edges always have physical containers - using edge_storage_type = EdgeIter; - edge_storage_type edge_storage_; - vertex_descriptor source_; // Source vertex descriptor - - auto source_id() const; // Delegates to source_.vertex_id() - auto target_id() const; // Extracts from *edge_storage_ - auto edge_value() const; // Edge property access via *edge_storage_ +template +class edge_descriptor { + using edge_storage_type = EdgeIter; // Always iterator, never conditional + EdgeIter edge_storage_; + vertex_descriptor source_; +public: + auto source_id() const; // Owning vertex ID + auto target_id(VertexData&) const; // Extracted from edge data + auto inner_value(VertexData&) const; // Edge property value }; ``` -**Edge Descriptor Characteristics:** -- **Carries source**: Unlike BGL2 which stores source in edge data, graph-v3 stores source vertex descriptor directly -- **Always iterator-based**: Edge storage is always the edge iterator (unlike vertex descriptors which may be index-only) -- **Flexible target extraction**: Supports simple integral targets, pair-like edges, and custom edge types -- **Size**: Iterator-dependent; typically 8 bytes per iterator + 8 bytes for source vertex index +**Direction tags** (`out_edge_tag`, `in_edge_tag`) determine source/target semantics for bidirectional graphs. -### Comparative Analysis - -#### Vertex Descriptors - -| Aspect | BGL2 | Graph-v3 | -|--------|------|----------| -| **Type** | Selector-dependent: `size_t` (vecS) or iterator (listS/setS) | `vertex_descriptor` template | -| **Internal storage** | Index (8 bytes) for vecS; iterator for listS/setS | Index for random-access (8 bytes), iterator otherwise | -| **Container selectors** | 10 selectors (vecS, listS, setS, mapS, multisetS, multimapS, hash_setS, hash_mapS, hash_multisetS, hash_multimapS); 3 vertex list impls (vecS, listS, setS) | 25 trait combinations (vov, vofl, mofl, mol, dos, uous, etc.) | -| **Non-integral IDs** | Not directly supported (vertex descriptors are index or iterator) | Native support via map-based traits (mofl, mol, uofl, etc.) | -| **Property access** | Requires property map lambda | `inner_value(container)` or CPO `vertex_value(g, u)` | -| **Array indexing** | Direct (`container[descriptor]`) | Via `vertex_id()` for random-access containers | -| **Descriptor stability** | Stable (index-based) | Stable for random-access; iterator-dependent for others | -| **Compile-time safety** | Optional `strong_descriptor` wrapper | Inherent (distinct template instantiations) | +**Key Characteristics:** +- Template-based: distinct type per container combination +- Vertex descriptor: `size_t` for random-access, iterator for forward-only containers +- Edge descriptor: always stores edge iterator + source vertex descriptor +- Flexible ID types: supports non-integral vertex IDs (strings, custom keys) via map-based containers +- Synthesized on iteration: `vertex_descriptor_view` / `edge_descriptor_view` create descriptors lazily +- No explicit null vertex convention (no null sentinel) -#### Edge Descriptors +### Comparative Analysis | Aspect | BGL2 | Graph-v3 | |--------|------|----------| -| **Type** | `adjacency_list_edge` struct | `edge_descriptor` template | -| **Source storage** | In edge data (`source` field) | Embedded `vertex_descriptor` member | -| **Target storage** | In edge data (`target` field) | Extracted via `target_id(vertex_data)` | -| **Parallel edge support** | `edge_index` field distinguishes duplicates | Iterator position distinguishes duplicates | -| **Size (vector-based)** | 24 bytes (source + target + index) | Iterator-dependent (typically 16 bytes: 8 for edge iterator + 8 for source vertex index) | -| **Size (iterator-based)** | Varies by iterator size (for listS/setS) | Varies by iterator size | -| **Property access** | `g[e].property` with property map | CPO `edge_value(g, uv)` or `inner_value()` | +| **Vertex descriptor type** | `size_t` (vecS) or iterator (listS/setS) | `vertex_descriptor` with conditional storage | +| **Edge descriptor type** | `adjacency_list_edge` struct | `edge_descriptor` template | +| **Vertex descriptor size** | 8 bytes (vecS) or iterator-sized | 8 bytes (random-access) or iterator-sized | +| **Edge descriptor fields** | source + target + edge_index | edge_iterator + source vertex_descriptor | +| **Edge size (vector-based)** | 24 bytes (source + target + index) | ~16 bytes (edge iter + source index) | +| **Non-integral vertex IDs** | Not directly supported | Native (map/unordered_map vertices) | +| **Compile-time safety** | Optional `descriptor` wrapper | Inherent (distinct template instantiations) | +| **Null descriptor** | `static_cast(-1)` for vecS | No built-in convention | +| **Parallel edge control** | `is_unique` trait on selector | Container choice (set vs vector) | +| **Property access** | `g[v].prop` or property map lambda | CPO `vertex_value(g, u)` / `edge_value(g, uv)` | --- -## 2. Ability to Adapt to Pre-existing Graph Data Structures +## 2. Ability to Adapt Pre-existing Graph Data Structures ### BGL2 Adaptation Model -BGL2 uses a **traits specialization + ADL** pattern: +BGL2 uses **`graph_traits` specialization + ADL free functions**: ```cpp // Step 1: Specialize graph_traits @@ -137,64 +141,78 @@ struct graph_traits { // Step 2: Provide ADL free functions auto vertices(const MyGraph& g) { return g.get_vertices(); } -auto edges(const MyGraph& g) { return g.get_edges(); } -auto source(const MyGraph& g, edge_descriptor e) { return g.get_source(e); } -auto target(const MyGraph& g, edge_descriptor e) { return g.get_target(e); } -auto out_edges(const MyGraph& g, vertex_descriptor v) { return g.outgoing(v); } +auto num_vertices(const MyGraph& g) { return g.vertex_count(); } +auto out_edges(vertex_descriptor v, const MyGraph& g) { return g.outgoing(v); } +auto source(edge_descriptor e, const MyGraph& g) { return g.get_source(e); } +auto target(edge_descriptor e, const MyGraph& g) { return g.get_target(e); } ``` **Strengths:** -- One-time traits specialization exposes all required types -- ADL functions provide complete customization control -- Well-established pattern (compatible with legacy Boost.Graph adaptors) -- Works with any descriptor type the user chooses +- Well-established pattern familiar to Boost.Graph users +- Complete control over all type definitions via traits +- Works with any descriptor type +- Explicit about what a graph type provides **Limitations:** -- Must specialize `graph_traits` (potentially invasive to library code) +- Requires `graph_traits` specialization (potentially invasive) - All required functions must be provided upfront - No fallback behavior if a function isn't provided +- Standard containers need an adapter wrapper ### Graph-v3 Adaptation Model -Graph-v3 uses a **CPO with member → ADL → default resolution**: +Graph-v3 uses **CPOs with a member → ADL → default resolution chain**: ```cpp // CPO resolution order for vertices(g): -// 1. g.vertices() // Member function (preferred) -// 2. vertices(g) // ADL function -// 3. vertex_descriptor_view(g) // Default (container auto-detection) - -// CPO resolution order for vertex_id(g, u): -// 1. vertex_id(g, u) // ADL function -// 2. u.vertex_id() // Descriptor method (default) +// 1. g.vertices() — Member function (preferred) +// 2. vertices(g) — ADL function +// 3. vertex_descriptor_view(g) — Default (container auto-detection) + +// CPO resolution order for target_id(g, uv): +// 1. (*uv.value()).target_id() — Edge data method +// 2. ADL target_id(g, uv) — ADL function +// 3. uv.target_id() — Descriptor method +// 4. Tuple get<1> — Structured binding fallback ``` -**Adaptation Example:** -```cpp -// Option 1: Define a member function -class MyGraph { - auto vertices() { return my_vertex_range(); } -}; +**Three levels of adaptation:** -// Option 2: Define ADL friend function -class MyGraph { - friend auto vertices(MyGraph& g) { return g.internal_vertices(); } -}; +1. **Zero configuration** — Standard containers work directly: + ```cpp + std::vector> g = {{1,2}, {2,3}, {3}}; + for (auto u : graph::vertices(g)) + for (auto e : graph::edges(g, u)) + auto tid = graph::target_id(g, e); // Just works + ``` -// Option 3: Works automatically if MyGraph is a container -// (vector, list, map, etc. with inner_value_pattern) -``` +2. **Concept satisfaction** — Any type satisfying `adjacency_list` concept: + ```cpp + template + concept adjacency_list = + vertex_range && + requires(G& g, vertex_t u) { + { edges(g, u) } -> targeted_edge_range; + }; + ``` + +3. **Member or friend functions** — Custom graphs provide methods: + ```cpp + class MyGraph { + friend auto vertices(MyGraph& g) { return g.internal_vertices(); } + }; + ``` **Strengths:** -- Non-invasive: No traits specialization required -- Progressive: Only provide what you need, defaults handle the rest -- Container-friendly: Standard containers work automatically -- Member functions preferred (encapsulation respected) +- Non-invasive: no traits specialization required +- Progressive: only provide what you need; defaults handle the rest +- Standard containers work automatically via pattern detection +- Member functions respected (encapsulation preserved) **Limitations:** -- Default behavior requires container to satisfy `inner_value_pattern` concepts -- Descriptor view wrapping may add overhead for simple adaptations -- Less explicit about what a graph type provides (determined at instantiation) +- Default behavior requires containers to satisfy `inner_value_pattern` concepts +- Less explicit about what a graph type must provide (determined at instantiation) +- CPO resolution chain adds library implementation complexity ### Comparative Analysis @@ -202,10 +220,10 @@ class MyGraph { |--------|------|----------| | **Primary mechanism** | `graph_traits` specialization | CPO with fallback chain | | **Function customization** | ADL only | Member → ADL → default | -| **Invasiveness** | Must specialize traits | Non-invasive | -| **Standard containers** | Requires adapter wrapper | Automatic detection | +| **Invasiveness** | Must specialize traits template | Non-invasive | +| **Standard containers** | Require adapter wrapper | Automatic detection | +| **Compile-time checking** | Concepts on traits | CPO `requires` clauses | | **Discoverability** | Explicit traits documentation | Concept-based detection | -| **Compile-time checking** | Via concepts on traits | Via CPO `requires` clauses | | **Legacy compatibility** | High (BGL1 pattern) | New pattern (C++20) | --- @@ -214,101 +232,109 @@ class MyGraph { ### BGL2 Strengths -1. **Complete algorithm library**: BFS, DFS, Dijkstra, Bellman-Ford, Prim, Kruskal, topological sort, connected components are implemented and tested. - -2. **Simple property maps**: Lambda-based `PropertyMap` concept using `std::invocable` - any callable works: +1. **Complete algorithm library with rich result types.** BFS, DFS, Dijkstra, Bellman-Ford, Prim, Kruskal, topological sort, connected components, and strongly connected components are all implemented. Algorithm results are first-class objects with query methods: ```cpp - auto weight = [&g](edge_descriptor e) { return g[e].weight; }; - dijkstra_shortest_paths(g, source, distances, weight); + auto result = dijkstra_shortest_paths(g, source, weight); + if (result.is_reachable(target)) + auto path = result.path_to(target); // Returns vector ``` -3. **Established patterns**: Familiar to Boost.Graph users, extensive documentation pattern. +2. **Composable, parallel, and coroutine algorithms.** Beyond traditional algorithms: + - **Composable:** `component_view`, `find_distances_from()`, `find_k_core()`, `find_neighbors()`, `degree_distribution()` — ranges-based composition + - **Parallel:** `parallel_bfs()` (level-synchronous), `parallel_connected_components()` (Shiloach-Vishkin) via `std::execution` policies + - **Coroutine:** `bfs_traverse()`, `dfs_traverse()` as lazy generators with early termination support -4. **Algorithm composability**: Modern visitor callbacks with designated initializers: +3. **Named parameters via C++20 designated initializers.** Algorithms accept parameter structs with sensible defaults: ```cpp - bfs(g, source, { - .on_discover_vertex = [](auto v, auto& g) { ... }, - .on_tree_edge = [](auto e, auto& g) { ... } + auto result = dijkstra_shortest_paths(g, source, { + .weight_map = [&g](auto e) { return g[e].weight; }, + .distance_compare = std::less{}, + .distance_infinity = std::numeric_limits::max() }); ``` -5. **Lightweight descriptors**: 8-byte vertex descriptors for `vecS` enable cache-efficient traversal. +4. **Graph validation framework.** `validate_graph()` checks structural invariants: + - Dangling edges (edges to non-existent vertices) + - Self-loops and parallel edges (where disallowed) + - Bidirectional consistency (in-edges match out-edges) + - Vertex/edge count mismatches + +5. **MutableGraph concept.** Individual `add_vertex()`, `remove_vertex()`, `add_edge()`, `remove_edge()` operations with well-defined invalidation semantics per selector. -6. **Selector-based configuration**: Compile-time container selection via template parameters with concept-based constraints: +6. **Simple lambda-based property maps.** Any callable satisfying `std::invocable` works as a property map — no special types needed: ```cpp - template - concept ContainerSelector = - std::same_as || std::same_as || - std::same_as || /* ... 7 more selectors */; + auto weight = [&g](edge_descriptor e) { return g[e].weight; }; ``` -7. **Automatic parallel edge detection**: `parallel_edge_category_for` determines `allow_parallel_edge_tag` vs `disallow_parallel_edge_tag` based on selector's `is_unique` trait. +7. **Automatic parallel edge detection.** `parallel_edge_category_for` determines `allow_parallel_edge_tag` vs `disallow_parallel_edge_tag` based on the selector's `is_unique` trait. + +8. **Type-safe strong descriptors.** Optional `descriptor` wrapper prevents accidental vertex/edge descriptor mix-ups at compile time, with built-in null semantics. ### BGL2 Weaknesses -1. **No direct non-integral vertex ID support**: While `mapS` is defined for edge containers, vertex IDs remain index-based (for `vecS`) or iterator-based (for `listS`/`setS`). Non-integral vertex IDs (strings, custom types) would require: - - A separate vertex-to-index mapping layer - - External property maps for string/custom key lookup +1. **No direct non-integral vertex ID support.** Vertex descriptors are `size_t` (vecS) or iterators (listS/setS). Non-integral vertex IDs (strings, UUIDs, custom types) require an external mapping layer. -2. **Traits boilerplate**: Adapting external graphs requires explicit `graph_traits` specialization. +2. **Traits boilerplate for adaptation.** External graph types require explicit `graph_traits` specialization — more invasive than concept-based adaptation. -3. **Single-partition assumption**: No built-in support for partitioned/distributed graphs. +3. **Limited vertex list selectors.** Only 3 vertex list implementations (`vecS`, `listS`, `setS`) despite 10 out-edge selectors. Hash-based vertex storage is not available. -4. **Vertex list selector limitation**: While 10 out-edge selectors are supported, only 3 vertex list selectors are implemented (`vecS`, `listS`, `setS`). Hash-based vertex storage is not yet available. +4. **No zero-config container support.** Standard containers like `vector>` cannot be used directly — they must be wrapped in `adjacency_list`. + +5. **No CSR in-place construction from edge ranges.** The `compressed_sparse_row_graph` requires explicit construction rather than loading from arbitrary edge ranges. ### Graph-v3 Strengths -1. **Multi-layered container flexibility**: - - **Zero-config**: Standard containers like `vector>` work directly as graphs - - **Concept-based adaptation**: Any structure with random-access vertices + forward-range edges works automatically - - **Trait-based**: 25 explicit trait combinations for `dynamic_graph` - - **CSR format**: `compressed_graph` for high-performance static graphs - - Example - standard container as graph: - ```cpp - std::vector> g = {{1,2}, {2,3}, {3}}; - for (auto u : graph::vertices(g)) { - for (auto e : graph::edges(g, u)) { - auto tid = graph::target_id(g, e); // Just works! - } - } - ``` +1. **Multi-layered container flexibility.** Three levels of graph support: + - **Zero-config:** `vector>` works directly as an adjacency list via CPOs + - **Concept-based:** Any type satisfying `adjacency_list` concept works automatically + - **Trait-based:** 27+ trait combinations for `dynamic_graph` (vov, vofl, mol, etc.) + - **CSR:** `compressed_graph` for high-performance static graphs -2. **Non-invasive adaptation**: CPO resolution with fallbacks means: - - Standard containers work without any wrapper - - Custom graphs just need to satisfy `adjacency_list` concept +2. **Non-invasive adaptation.** CPO resolution with member → ADL → default fallbacks means: + - Standard containers work without wrappers + - Custom graphs just need to satisfy concepts - No traits specialization required -3. **Rich descriptor pattern detection**: Automatic handling of: - - Random-access patterns (vector, deque) → index-based IDs - - Pair-value patterns (map) → key-based IDs - - Whole-value patterns (set) → whole-element IDs +3. **Native non-integral vertex IDs.** Map-based containers (mol, mofl, uofl, etc.) provide string keys, custom types, or sparse integer IDs as first-class vertex identifiers. -4. **Efficient descriptors for random-access**: When using vector-based storage, `vertex_descriptor` stores only a `size_t` (same as BGL2): - ```cpp - for (auto v : graph::vertices(g)) { - auto id = v.vertex_id(); // Returns index (no graph needed) - auto& val = v.inner_value(vertices); // Requires container reference - } - ``` +4. **Rich traversal views.** 13 view headers providing lazy range adaptors: + - `vertices_dfs()`, `edges_dfs()` — DFS traversal as input_range + - `vertices_bfs()`, `edges_bfs()` — BFS traversal as input_range + - `vertexlist()`, `incidence()`, `neighbors()`, `edgelist()` — structural views + - `transpose()` — reversed-direction adaptor + - Pipe syntax support: `g | vertexlist() | ...` + +5. **Comprehensive algorithm set.** 14 algorithms including several not in BGL2: + - Triangle counting (directed + undirected) + - Articulation points (iterative Hopcroft-Tarjan) + - Biconnected components (Hopcroft-Tarjan with edge stack) + - Jaccard similarity (per-edge coefficient) + - Label propagation (community detection) + - Maximum independent set (greedy) -5. **Partition support**: CPOs support `vertices(g, partition_id)` for multi-partition graphs. +6. **Adaptive property maps.** `vertex_property_map` automatically selects `vector` for index-based graphs or `unordered_map` for mapped graphs. -6. **Non-integral vertex ID support**: Native support for string keys, custom types, etc. +7. **Bidirectional graph support.** `undirected_adjacency_list` with dual-list design provides O(1) edge removal from both endpoints. `dynamic_graph` supports `Bidirectional=true` template parameter. -7. **Compressed Sparse Row support**: `compressed_graph` provides optimal memory layout for static graphs. +8. **Edge list module.** Standalone `edge_list` support for `pair`, `tuple`, and `edge_data` structs with dedicated CPOs and concepts. ### Graph-v3 Weaknesses -1. **No algorithms implemented**: This is a known limitation. All traversal, shortest-path, spanning tree, and flow algorithms are missing. +1. **Batch-only mutation.** `dynamic_graph` provides `load_edges()` and `load_vertices()` for bulk loading but no individual `add_vertex()` / `add_edge()` / `remove_vertex()` / `remove_edge()`. This limits interactive graph construction. + +2. **No composable, parallel, or coroutine algorithms.** Graph-v3 has traditional algorithms but lacks the higher-level paradigms (ranges composition, `std::execution` policies, lazy generators). + +3. **No named parameter pattern.** Algorithms use positional parameters with defaults rather than C++20 designated initializer structs. -2. **Iterator-based descriptors for non-random-access containers**: Map/list storage requires storing iterators (larger than 8 bytes). +4. **No algorithm result types.** Algorithms write to output parameters (distance maps, predecessor maps) rather than returning rich result objects with query methods like `path_to()` or `is_reachable()`. -3. **Iterator stability concerns**: Descriptors holding iterators may be invalidated by container modifications. +5. **No graph validation framework.** No equivalent to BGL2's `validate_graph()` for checking structural invariants. -4. **Learning curve**: CPO resolution chain and pattern detection are less familiar than traditional traits. +6. **No strong descriptor typing.** Vertex and edge descriptors are distinct template instantiations (providing some safety), but there is no explicit `descriptor` wrapper with null semantics. -5. **Property map story unclear**: No explicit property map abstraction; properties accessed via descriptors or container access. +7. **Iterator stability concerns.** Descriptors holding iterators for non-random-access containers may be invalidated by container modifications. + +8. **Learning curve.** CPO resolution chains and automatic pattern detection are powerful but less familiar than traditional traits-based approaches. --- @@ -316,10 +342,10 @@ class MyGraph { ### BGL2 Selector-Based Approach -BGL2 uses **container selector tags** that map to standard containers: +BGL2 uses **container selector tags** that map to standard containers via `container_gen`: ```cpp -// Selector tags with compile-time properties +// Each selector carries compile-time traits struct vecS { static constexpr bool is_random_access = true; static constexpr bool has_stable_iterators = false; @@ -329,238 +355,383 @@ struct vecS { // container_gen maps selectors to containers template -struct container_gen { - using type = std::list; +struct container_gen { + using type = std::vector; }; -// Usage: adjacency_list +// Usage: adjacency_list adjacency_list g; -// ^^^^^ ^^^^^ -// edges vertices ``` -**Available Selectors (10 total):** - -| Selector | Container | Use Case | -|----------|-----------|----------| -| `vecS` | `std::vector` | Fast access, cache-friendly traversal | -| `listS` | `std::list` | Stable iterators, O(1) insertion/removal | -| `setS` | `std::set` | Unique edges, ordered iteration | -| `mapS` | `std::set` (for edges) | Prevent parallel edges | -| `multisetS` | `std::multiset` | Ordered with parallel edges | -| `multimapS` | `std::multiset` | Key-value with parallel edges | -| `hash_setS` | `std::unordered_set` | Fast unique lookup | -| `hash_mapS` | `std::unordered_set` | Fast key-value access | -| `hash_multisetS` | `std::unordered_multiset` | Fast with duplicates | -| `hash_multimapS` | `std::unordered_multiset` | Fast key-value with duplicates | - -**Vertex List Implementations (3):** `vecS`, `listS`, `setS` +**All 10 Selectors with Traits:** + +| Selector | Container | random_access | stable_iters | ordered | unique | +|----------|-----------|:---:|:---:|:---:|:---:| +| `vecS` | `vector` | ✓ | ✗ | ✗ | ✗ | +| `listS` | `list` | ✗ | ✓ | ✗ | ✗ | +| `setS` | `set` | ✗ | ✓ | ✓ | ✓ | +| `mapS` | `set` | ✗ | ✓ | ✓ | ✓ | +| `multisetS` | `multiset` | ✗ | ✓ | ✓ | ✗ | +| `multimapS` | `multiset` | ✗ | ✓ | ✓ | ✗ | +| `hash_setS` | `unordered_set` | ✗ | ✗ | ✗ | ✓ | +| `hash_mapS` | `unordered_set` | ✗ | ✗ | ✗ | ✓ | +| `hash_multisetS` | `unordered_multiset` | ✗ | ✗ | ✗ | ✗ | +| `hash_multimapS` | `unordered_multiset` | ✗ | ✗ | ✗ | ✗ | + +> **Note:** Despite "map" in their names, `mapS`/`multimapS`/`hash_mapS`/`hash_multimapS` all map to **set** containers. The "map" name reflects the vertex→edge *mapping semantic*, not the container type. + +**Selector concepts** enforce constraints: +- `ContainerSelector` — union of all 10 types +- `SequenceSelector` — vecS, listS +- `AssociativeSelector` — set/map variants +- `UnorderedSelector` — hash_* variants +- `StableIteratorSelector` — selectors where `has_stable_iterators == true` +- `RandomAccessSelector` — selectors where `is_random_access == true` + +**Vertex list implementations:** Only `vecS`, `listS`, and `setS` are supported for vertex storage. All 10 selectors work for out-edge lists. + +**Additional graph containers:** +- `adjacency_matrix` — O(V²) space, O(1) edge lookup, fixed vertex count +- `compressed_sparse_row_graph` — CSR format, static, cache-efficient +- `grid_graph` — N-dimensional implicit grid, O(1) space, optional torus wrapping ### Graph-v3 Multi-Layered Approach -Graph-v3 provides **three levels of graph support**: +Graph-v3 provides **four levels** of graph support with increasing configuration: -#### Level 1: Standard Containers as Graphs (Zero Configuration) +**Level 1: Standard Containers as Graphs (Zero Configuration)** -Standard containers work directly as graphs via CPOs and concepts: +Any combination of random-access vertex container + forward-range edge container works directly: ```cpp -// std::vector> works directly as an adjacency list -std::vector> g = { - {1, 2}, // vertex 0 -> edges to 1, 2 - {2, 3}, // vertex 1 -> edges to 2, 3 - {3} // vertex 2 -> edges to 3 -}; +// Simple adjacency list +std::vector> g = {{1,2}, {2,3}, {3}}; -// CPOs work automatically -for (auto u : graph::vertices(g)) { - auto id = graph::vertex_id(g, u); // Returns index (size_t) - for (auto e : graph::edges(g, u)) { - auto tid = graph::target_id(g, e); // Returns target vertex ID - } -} - -// Weighted graphs: vector>> +// Weighted adjacency list std::vector>> weighted = { - {{1, 1.5}, {2, 2.0}}, // vertex 0 with weights - {{2, 0.5}} // vertex 1 + {{1, 1.5}, {2, 2.0}}, {{2, 0.5}}, {} }; -``` -**Supported patterns:** -- `vector>` - simple adjacency list -- `vector>>` - weighted adjacency -- `vector>` - forward_list edges -- `deque>` - deque-based vertices -- Any combination with random-access vertices + forward-range edges +// CPOs work automatically on both +for (auto u : graph::vertices(g)) + for (auto e : graph::edges(g, u)) + auto tid = graph::target_id(g, e); +``` -#### Level 2: Adapt Existing Graphs (Concept-Based) +Supported patterns include `vector>`, `vector>>`, `vector>`, `deque>`, and any similar combination. -Any graph structure satisfying the `adjacency_list` concept works automatically: +**Level 2: Concept-Based Adaptation** +Any type satisfying the `adjacency_list` concept works: ```cpp -// Requirements: random-access vertices, forward-range edges template concept adjacency_list = - vertex_range && // vertices(g) returns a range + vertex_range && requires(G& g, vertex_t u) { - { edges(g, u) } -> targeted_edge_range; // forward range of edges + { edges(g, u) } -> targeted_edge_range; }; ``` -Custom graphs just need to provide `vertices()` and `edges()` functions/methods. - -#### Level 3: dynamic_graph with Trait Structs - -For full-featured graphs with properties, use trait-based configuration: +**Level 3: `dynamic_graph` with Trait Structs** +For full-featured mutable graphs with properties: ```cpp -// Trait struct defines container types directly -template -struct mofl_graph_traits { - using vertices_type = std::map; // map for vertices - using edges_type = std::forward_list; // forward_list for edges -}; +template +class dynamic_graph; -// Usage: dynamic_graph -using G = dynamic_graph>; +// Traits define vertex and edge container types +using G = dynamic_graph>; ``` -**Available Trait Combinations (25 total):** +**27+ trait combinations:** + +| Vertices | Edge Containers | Trait Names | +|----------|----------------|-------------| +| `vector` | vector, list, forward_list, deque, set, unordered_set, map | vov, vol, vofl, vod, vos, vous, vom | +| `deque` | vector, list, forward_list, deque, set, unordered_set | dov, dol, dofl, dod, dos, dous | +| `map` | vector, list, forward_list, deque, set, unordered_set, map | mov, mol, mofl, mod, mos, mous, mom | +| `unordered_map` | vector, list, forward_list, deque, unordered_set | uov, uol, uofl, uod, uous | + +Separate undirected-specific traits also exist (uios, uous, uov, uod, uol, uofl). -| Vertices | Edges | Trait | -|----------|-------|-------| -| vector | vector, list, forward_list, deque, set, unordered_set, edge_multimap | vov, vol, vofl, vod, vos, vous, vom | -| deque | vector, list, forward_list, deque, set, unordered_set | dov, dol, dofl, dod, dos, dous | -| map | vector, list, forward_list, deque, set, unordered_set, edge_multimap | mov, mol, mofl, mod, mos, mous, mom | -| unordered_map | vector, list, forward_list, deque, unordered_set | uov, uol, uofl, uod, uous | +**Level 4: `compressed_graph` (CSR)** -#### Level 4: compressed_graph (CSR Format) +High-performance static graph: +```cpp +template +class compressed_graph; +// row_index_ + col_index_ + optional values — optimal for read-heavy workloads +``` -High-performance static graph with Compressed Sparse Row storage: +**Level 5: `undirected_adjacency_list`** +Dual-list design with O(1) edge removal from both endpoints: ```cpp -compressed_graph csr; -// Optimal for read-heavy workloads, minimal memory footprint +template class VContainer, typename Alloc> +class undirected_adjacency_list; ``` ### Comparative Analysis | Aspect | BGL2 Selectors | Graph-v3 | |--------|---------------|----------| -| **Configuration** | Tag-based template params | Multi-layered (zero-config to traits) | +| **Configuration style** | Tag-based template params | Multi-layered (zero-config to traits) | | **Standard containers** | Requires `adjacency_list` wrapper | Direct use via concepts | -| **Total built-in options** | 10 × 3 = 30 potential (edge × vertex) | 25 traits + unlimited container combos | +| **Edge container options** | 10 selectors | 7+ edge containers per vertex type | +| **Vertex container options** | 3 (vecS, listS, setS) | 4 (vector, deque, map, unordered_map) | +| **Total built-in combos** | 10 × 3 = 30 (edge × vertex) | 27+ traits + unlimited zero-config | | **Extensibility** | Specialize `container_gen` | Satisfy concept or create trait | | **CSR/static graphs** | `compressed_sparse_row_graph` | `compressed_graph` | -| **Adaptation complexity** | graph_traits specialization | Just satisfy concept | -| **Non-integral vertex IDs** | Not directly supported | Native (map/unordered_map vertices) | +| **Dense graphs** | `adjacency_matrix` | Not available | +| **Implicit graphs** | `grid_graph` | Not available | +| **Non-integral vertex IDs** | Not supported | Native (map/unordered_map vertices) | +| **Undirected O(1) removal** | Via bidirectional + listS | `undirected_adjacency_list` | --- ## 5. Other Design and Flexibility Considerations -### Customization Point Objects (Graph-v3) vs ADL (BGL2) +### Customization Point Objects vs ADL -**Graph-v3 CPO Pattern:** +**Graph-v3** uses qualified CPO calls that prevent ADL hijacking: ```cpp -// Guaranteed to find the right function, prevents ADL hijacking -auto verts = graph::vertices(my_graph); // Qualified call to CPO +auto verts = graph::vertices(my_graph); // Qualified — CPO dispatches correctly ``` -**BGL2 ADL Pattern:** +**BGL2** uses unqualified ADL lookup: ```cpp -// Relies on unqualified lookup -auto verts = vertices(my_graph); // ADL finds correct overload +auto verts = vertices(my_graph); // ADL finds correct overload in graph's namespace ``` -CPOs provide stronger encapsulation and avoid ADL hijacking issues, but add complexity to the library implementation. +CPOs provide stronger encapsulation and prevent name collisions, but add complexity to the library implementation. BGL2's ADL pattern is simpler and more familiar. + +### Concept Hierarchies -### Direction and Edge Parallel Categories +**BGL2 concepts** follow a traditional graph theory hierarchy: -**BGL2**: Uses tag types (`directed_tag`, `bidirectional_tag`, `allow_parallel_edge_tag`) extracted via `graph_traits`. +| Concept | Requires | +|---------|----------| +| `Graph` | `vertex_descriptor`, `edge_descriptor`, `directed_category` | +| `IncidenceGraph` | Graph + `out_edges()`, `out_degree()`, `source()`, `target()` | +| `BidirectionalGraph` | IncidenceGraph + `in_edges()`, `in_degree()` | +| `VertexListGraph` | Graph + `vertices()`, `num_vertices()` | +| `EdgeListGraph` | Graph + `edges()`, `num_edges()` | +| `AdjacencyGraph` | Graph + `adjacent_vertices()` | +| `MutableGraph` | Graph + `add_vertex()`, `remove_vertex()`, `add_edge()`, `remove_edge()` | -**Graph-v3**: Uses template parameters directly (`bool Sourced`) and trait-based container selection. Direction is implicit in which CPOs are available (`source_id` requires `Sourced=true`). +**Graph-v3 concepts** are organized around container patterns: -### Property Access Paradigms +| Concept | Requires | +|---------|----------| +| `vertex_range` | Forward, sized range with `vertex_id` | +| `index_vertex_range` | Integral `vertex_id_t`, random-access container | +| `mapped_vertex_range` | Hashable `vertex_id_t`, `find_vertex()` | +| `adjacency_list` | `vertex_range` + `out_edges()` → `targeted_edge_range` | +| `index_adjacency_list` | `adjacency_list` + `index_vertex_range` | +| `mapped_adjacency_list` | `adjacency_list` + `mapped_vertex_range` | +| `bidirectional_adjacency_list` | `adjacency_list` + `in_edges()` | +| `ordered_vertex_edges` | Adjacency lists sorted by target_id | -**BGL2**: Unified property map concept with `get(pmap, key)` and `put(pmap, key, value)`: +Key difference: Graph-v3 distinguishes index-based vs mapped vertex ranges at the concept level, enabling algorithms to select optimal data structures (e.g., vector vs unordered_map for property maps). + +### Property Access + +**BGL2:** Lambda-based property maps — any `std::invocable` works: ```cpp -template -concept PropertyMap = std::invocable && - std::convertible_to, Value>; +auto weight = [&g](edge_descriptor e) { return g[e].weight; }; +auto color = [&g](vertex_descriptor v) -> Color& { return g[v].color; }; +dijkstra_shortest_paths(g, source, weight); ``` -**Graph-v3**: Properties accessed via CPOs with graph reference: +**Graph-v3:** CPO-based access with adaptive property maps: ```cpp -for (auto u : graph::vertices(g)) { - graph::vertex_value(g, u) = new_value; // Via CPO (requires graph) -} +// CPO access (requires graph reference) +graph::vertex_value(g, u) = new_value; +graph::edge_value(g, uv); + +// Adaptive property map (auto-selects vector or unordered_map) +auto dist = make_vertex_property_map(g, std::numeric_limits::max()); +// Uses vector for index graphs, unordered_map for mapped graphs ``` -### Algorithm Integration Readiness +Graph-v3's `vertex_property_map` is adaptive but requires the graph type at creation. BGL2's lambda approach is simpler and more flexible. -| Aspect | BGL2 | Graph-v3 | -|--------|------|----------| -| **Color maps** | `vector_property_map` | Would need external map | -| **Distance maps** | `vector_property_map` | Would need external map | -| **Predecessor maps** | `vector_property_map` | Would need external map | -| **Weight access** | Lambda property map | `edge_value(g, e)` CPO | -| **Visitor callbacks** | Designated initializer struct | Not defined yet | +### Direction and Edge Categories -### Compile-Time Guarantees +**BGL2:** Tag types extracted via `graph_traits`: +- `directed_tag`, `undirected_tag`, `bidirectional_tag` (inherits from `directed_tag`) +- `allow_parallel_edge_tag` / `disallow_parallel_edge_tag` (derived from selector's `is_unique`) -**BGL2**: Concepts check that graphs satisfy required operations: +**Graph-v3:** Template parameters and direction tags: +- `bool Bidirectional` template parameter on `dynamic_graph` +- `out_edge_tag` / `in_edge_tag` on edge descriptors determine source/target semantics +- No explicit parallel edge category — controlled by container choice (set vs vector) + +### Visitor/Callback Patterns + +**BGL2 callbacks** use C++20 designated initializers with 9 event points: ```cpp -template -concept VertexListGraph = Graph && requires(G& g) { - { vertices(g) } -> std::ranges::input_range; - { num_vertices(g) } -> std::integral; -}; +auto result = breadth_first_search(g, source, { + .on_discover_vertex = [](auto v) { ... }, + .on_tree_edge = [](auto e, const auto& g) { ... }, + .on_finish_vertex = [](auto v) { ... } +}); +// Unspecified callbacks default to null_callback{} (zero overhead) ``` -**Graph-v3**: CPO `requires` clauses check individual operations: +**Graph-v3 visitors** use concept-checked optional callbacks: ```cpp -template -[[nodiscard]] constexpr auto operator()(G&& g) const - requires (_Choice._Strategy != _St::_none) -{ ... } +// Algorithms check for visitor methods via has_on_* concepts +void dijkstra_shortest_paths(G&& g, Sources& sources, + DistanceFn&&, PredecessorFn&&, WF&&, Visitor&& visitor = {}); +// Visitor events: on_initialize_vertex, on_discover_vertex, on_examine_vertex, +// on_examine_edge, on_edge_relaxed, on_finish_vertex +``` + +Both approaches have zero overhead for unused callbacks. BGL2's designated initializer pattern is more ergonomic; Graph-v3's concept-checked approach is more extensible. + +--- + +## 6. Algorithm Comparison + +### Algorithm Inventory + +| Algorithm | BGL2 | Graph-v3 | Notes | +|-----------|:----:|:--------:|-------| +| **BFS** | ✅ | ✅ | Both support visitors/callbacks | +| **DFS** | ✅ | ✅ | Both with edge classification (tree/back/forward/cross) | +| **Dijkstra shortest paths** | ✅ | ✅ | Graph-v3 supports multi-source; BGL2 has `dijkstra_result` | +| **Bellman-Ford shortest paths** | ✅ | ✅ | Both detect negative cycles | +| **Topological sort** | ✅ | ✅ | Graph-v3: full-graph, single-source, multi-source variants | +| **Connected components** | ✅ | ✅ | Graph-v3: DFS-based + parallel afforest | +| **Strongly connected components** | ✅ (Tarjan) | ✅ (Kosaraju) | Different algorithms; Graph-v3 has transpose + bidirectional overloads | +| **MST — Kruskal** | ✅ | ✅ | Graph-v3 includes in-place Kruskal variant | +| **MST — Prim** | ✅ | ✅ | Both priority-queue based | +| **Triangle counting** | — | ✅ | Undirected + directed variants | +| **Articulation points** | — | ✅ | Iterative Hopcroft-Tarjan | +| **Biconnected components** | — | ✅ | Hopcroft-Tarjan with edge stack | +| **Jaccard similarity** | — | ✅ | Per-edge coefficient | +| **Label propagation** | — | ✅ | Community detection | +| **Maximum independent set** | — | ✅ | Greedy algorithm | +| **Composable algorithms** | ✅ | — | k-core, degree distribution, component view, find_neighbors | +| **Parallel algorithms** | ✅ | — | parallel_bfs, parallel_connected_components (std::execution) | +| **Coroutine traversal** | ✅ | — | Lazy BFS/DFS generators with early termination | + +**Count:** BGL2 has 9 traditional + 3 paradigm categories. Graph-v3 has 14 traditional algorithms. + +### Algorithm Design Differences + +**Result types vs output parameters:** +- BGL2 returns rich result objects (`dijkstra_result`, `bfs_result`, `dfs_result`, etc.) with query methods: `path_to()`, `is_reachable()`, `distance_to()`, `has_cycle()` +- Graph-v3 writes to caller-provided output functions/maps (distance function, predecessor function, component function) + +**Named parameters vs positional:** +- BGL2 uses designated initializer structs with `use_default_t` sentinels: + ```cpp + dijkstra_shortest_paths(g, source, {.weight_map = w, .distance_compare = cmp}); + ``` +- Graph-v3 uses positional parameters with template defaults: + ```cpp + dijkstra_shortest_paths(g, sources, distance_fn, predecessor_fn, weight_fn, visitor, compare, combine); + ``` + +**Multi-source support:** +- Graph-v3 algorithms (Dijkstra, BFS, DFS, topological sort) accept a `Sources&` range for multi-source variants +- BGL2 coroutine traversals support multi-source; traditional algorithms are single-source + +**Traversal views (Graph-v3 only):** +Graph-v3 provides traversal as lazy ranges via views, complementing the algorithm functions: +```cpp +for (auto&& [uid, vid, uv] : graph::views::edges_dfs(g, source)) + process(uid, vid, uv); + +for (auto&& [uid] : graph::views::vertices_bfs(g, source)) + process(uid); + +// Topological sort as view +for (auto&& [uid] : graph::views::vertices_topological_sort(g)) + process(uid); +``` + +**Coroutine traversal (BGL2 only):** +BGL2 provides lazy generators for incremental traversal with early termination: +```cpp +for (auto v : bfs_traverse(graph, start)) { + if (v == target) break; // Early exit +} ``` +Both approaches enable lazy, on-demand traversal but with different mechanisms. + --- -## 6. Summary Recommendations +## 7. Summary and Recommendations ### When to Choose BGL2 -- You need **working algorithms now** (BFS, DFS, Dijkstra, Bellman-Ford, Prim, Kruskal, etc.) -- Your graphs use **integral or iterator-based vertex descriptors** -- You're migrating from **legacy Boost.Graph** code -- You want **familiar patterns** with established documentation -- You need **container selector flexibility** with 10 selectors for edges and 3 for vertices -- You want **automatic parallel edge detection** based on container uniqueness +- You need **working algorithms with rich result types** — `path_to()`, `is_reachable()`, `distance_to()`, `has_cycle()` out of the box +- You need **parallel algorithms** (`std::execution` policies) or **coroutine-based traversal** (lazy generators) +- You want **composable algorithms** (k-core decomposition, degree distribution, component views) +- Your graphs use **integral vertex descriptors** and you want **MutableGraph** operations (`add_vertex`, `add_edge`, `remove_vertex`, `remove_edge`) +- You want **graph validation** to check structural invariants programmatically +- You need **dense graph support** (`adjacency_matrix`) or **implicit grid graphs** (`grid_graph`) +- You're migrating from **legacy Boost.Graph** code (familiar patterns, compatible traits) +- You want **named parameters** via C++20 designated initializers ### When to Choose Graph-v3 -- You want to use **standard containers directly** (`vector>`, etc.) without wrappers -- You need **non-integral vertex IDs** (strings, custom types) as first-class citizens -- You want to **adapt existing graph structures** by just satisfying concepts -- You need **maximum container flexibility** (25 traits + unlimited container combos) -- You prefer **self-contained descriptors** that carry vertex IDs -- You're building a **new codebase** and can wait for algorithms -- You need **partitioned graph support** or **CSR format** (`compressed_graph`) +- You want to use **standard containers directly** (`vector>`) without wrappers or boilerplate +- You need **non-integral vertex IDs** (strings, UUIDs, custom types) as first-class citizens +- You want to **adapt existing graph structures** by satisfying concepts rather than specializing traits +- You need **maximum container flexibility** (27+ trait combinations + unlimited zero-config containers) +- You need algorithms not in BGL2: **triangle counting**, **articulation points**, **biconnected components**, **Jaccard similarity**, **label propagation**, **maximum independent set** +- You want **traversal views** as lazy ranges with pipe syntax +- You need **multi-source** Dijkstra, BFS, or topological sort +- You want **adaptive property maps** that auto-select vector vs unordered_map based on graph type +- You need a **CSR graph** loadable from arbitrary edge ranges ### Potential Synergies Both libraries share C++20 foundations and could potentially: -1. Share concept definitions (both have similar `Graph`, `IncidenceGraph` concepts) -2. Use BGL2 algorithms with Graph-v3 containers via adapter layer -3. Adopt BGL2's property map abstraction in Graph-v3 for algorithm integration + +1. **Share concept definitions** — both have similar vertex list, incidence, and bidirectional graph concepts +2. **Cross-library algorithms** — BGL2 algorithms could operate on Graph-v3 containers via a thin adapter satisfying `graph_traits` +3. **Adopt BGL2 result types** — Graph-v3 could benefit from `dijkstra_result`-style return values with query methods +4. **Adopt Graph-v3 CPOs** — BGL2 could adopt CPO dispatch for stronger encapsulation +5. **Share validation** — BGL2's `validate_graph()` framework could be adapted for Graph-v3 containers + +### Quick Reference: Feature Matrix + +| Feature | BGL2 | Graph-v3 | +|---------|:----:|:--------:| +| Zero-config standard containers | ✗ | ✓ | +| Non-integral vertex IDs | ✗ | ✓ | +| Trait/selector container config | ✓ (10 selectors) | ✓ (27+ traits) | +| MutableGraph (individual ops) | ✓ | ✗ (batch only) | +| Batch edge loading | ✗ | ✓ | +| Adjacency matrix | ✓ | ✗ | +| Implicit grid graph | ✓ | ✗ | +| CSR graph | ✓ | ✓ | +| Undirected O(1) removal | partial | ✓ | +| Strong typed descriptors | ✓ | ✗ | +| Graph validation | ✓ | ✗ | +| Algorithm result objects | ✓ | ✗ | +| Named algorithm parameters | ✓ | ✗ | +| Traversal views (lazy ranges) | ✗ | ✓ | +| Coroutine traversal | ✓ | ✗ | +| Parallel algorithms | ✓ | ✗ | +| Composable algorithms | ✓ | ✗ | +| Visitor/callback support | ✓ | ✓ | +| Multi-source algorithms | partial | ✓ | +| Adaptive property maps | ✗ | ✓ | +| CPO-based dispatch | ✗ | ✓ | +| Traditional algorithms | 9 | 14 | --- -## 7. Code Examples Side-by-Side +## 8. Code Examples Side-by-Side ### Creating a Graph @@ -569,83 +740,100 @@ Both libraries share C++20 foundations and could potentially: struct VertexData { std::string name; }; struct EdgeData { double weight; }; -// Using selectors: setS for edges (no parallel), listS for vertices (stable) adjacency_list g; -auto v0 = g.add_vertex({"A"}); -auto v1 = g.add_vertex({"B"}); -auto [e, inserted] = g.add_edge(v0, v1, {1.5}); +auto v0 = add_vertex({"A"}, g); +auto v1 = add_vertex({"B"}, g); +auto [e, inserted] = add_edge(v0, v1, {1.5}, g); ``` -**Graph-v3 (using standard containers directly):** +**Graph-v3 (standard container):** ```cpp -// Simple adjacency list - no library types needed! -std::vector> g = { - {1, 2}, // vertex 0 -> edges to 1, 2 - {2}, // vertex 1 -> edge to 2 - {} // vertex 2 -> no outgoing edges -}; - -// Weighted version -std::vector>> weighted = { - {{1, 1.5}, {2, 2.0}}, - {{2, 0.5}}, - {} +std::vector>> g = { + {{1, 1.5}, {2, 2.0}}, // vertex 0: edges to 1 (w=1.5), 2 (w=2.0) + {{2, 0.5}}, // vertex 1: edge to 2 (w=0.5) + {} // vertex 2: no outgoing edges }; ``` -**Graph-v3 (using dynamic_graph with traits):** +**Graph-v3 (dynamic_graph):** ```cpp -using G = dynamic_graph>; +using G = dynamic_graph>; G g; -g.vertices().resize(2); -graph::vertex_value(g, 0) = "A"; -graph::vertex_value(g, 1) = "B"; -g.vertices()[0].emplace_back(1, 1.5); // edge 0->1 with weight 1.5 +g.load_edges(edges_data, edge_projection); +g.load_vertices(vertex_data, vertex_projection); ``` -### Traversing Vertices +### Traversing Vertices and Edges **BGL2:** ```cpp for (auto v : vertices(g)) { - std::cout << g[v].name << "\n"; // Requires graph for property access + std::cout << g[v].name << ": "; + for (auto e : out_edges(v, g)) + std::cout << target(e, g) << " "; } ``` -**Graph-v3 (standard container):** +**Graph-v3:** ```cpp -std::vector> g = {{1,2}, {2}, {}}; for (auto u : graph::vertices(g)) { - auto vid = graph::vertex_id(g, u); - std::cout << "Vertex " << vid << " has " - << std::ranges::size(graph::edges(g, u)) << " edges\n"; + auto uid = graph::vertex_id(g, u); + for (auto e : graph::edges(g, u)) + std::cout << graph::target_id(g, e) << " "; } ``` -**Graph-v3 (dynamic_graph):** +### Running Dijkstra + +**BGL2:** ```cpp -for (auto v : graph::vertices(g)) { - std::cout << graph::vertex_value(g, v) << "\n"; // Via CPO with graph reference +auto weight = [&g](auto e) { return g[e].weight; }; +auto result = dijkstra_shortest_paths(g, source, {.weight_map = weight}); + +if (result.is_reachable(target)) { + auto path = result.path_to(target); + auto dist = result.distance_to(target); } ``` -### Running BFS +**Graph-v3:** +```cpp +auto dist = make_vertex_property_map(g, std::numeric_limits::max()); +auto pred = make_vertex_property_map(g, graph::vertex_id(g, *begin(graph::vertices(g)))); +auto weight = [](auto& e) { return graph::edge_value(g, e); }; + +std::vector sources = {source}; +graph::dijkstra_shortest_paths(g, sources, dist, pred, weight); +// Query results directly from dist and pred maps +``` + +### BFS with Callbacks **BGL2:** ```cpp -auto [distances, predecessors] = bfs(g, source); -// or with callbacks: -bfs(g, source, { - .on_discover_vertex = [](auto v, auto&) { std::cout << v << " "; } +auto result = breadth_first_search(g, source, { + .on_discover_vertex = [](auto v) { std::cout << v << " "; }, + .on_tree_edge = [](auto e, const auto& g) { + std::cout << source(e, g) << "->" << target(e, g) << "\n"; + } }); ``` -**Graph-v3:** +**Graph-v3 (view-based):** ```cpp -// Not implemented - algorithms are a known limitation +for (auto&& [uid, vid, uv] : graph::views::edges_bfs(g, source)) { + std::cout << uid << "->" << vid << "\n"; +} ``` ---- - -*Note: Algorithms have not been implemented for graph-v3 and this is a known limitation.* +**Graph-v3 (algorithm with visitor):** +```cpp +struct my_visitor { + void on_discover_vertex(const G&, vertex_id_t uid) { + std::cout << uid << " "; + } +}; +graph::breadth_first_search(g, sources, my_visitor{}); +```