diff --git a/CHANGELOG.md b/CHANGELOG.md index da2c42a..53283ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,8 +61,11 @@ - `graphviz_output.hpp` — Graphviz `.gv` file writers using vertexlist, incidence, and edges_dfs views - `germany_routes_example.cpp` — builds a Germany routes graph, traverses it, and runs Dijkstra twice (segment count and km distance) - **`adjacency_matrix` container** (`container/adjacency_matrix.hpp`) — added dense `n x n` graph container (C++20) with optional C++23 `md_adjacency_matrix` `mdspan` view variant; supports weighted/unweighted graphs and models graph-v3 adjacency CPO concepts. +- **`basic_edge` concept** (`detail/edge_cpo.hpp`, in `namespace graph`) — shared edge floor requiring only `source_id(g, e)` and `target_id(g, e)`. Used by both the adjacency-list `edge` concept and `edge_list::basic_sourced_edgelist`, tying the two abstract data types together at the edge level. +- **`vertex_value(g, uid)` convenience overload** — id-based form of the `vertex_value` CPO. Mirrors the descriptor dispatch: prefers a member `g.vertex_value(uid)` or ADL `vertex_value(g, uid)` taking the id directly, falling back to `vertex_value(g, *find_vertex(g, uid))` only when neither exists. ### Changed +- **`edge` concept split into `basic_edge` + `edge`** — the adjacency-list `edge` now refines the shared `graph::basic_edge` (source_id/target_id) and adds the `source(g, e)` / `target(g, e)` vertex descriptors. Bare edge-list elements (tuples/pairs/`edge_data`) satisfy `basic_edge` but not `edge`. `edge_list::basic_sourced_edgelist` now requires `basic_edge` and drops its previous `target_id`→`source_id` return-type convertibility clause (return types are intentionally unconstrained, matching the adjacency-list side). - **`undirected_adjacency_list` mutation API renamed** to match `dynamic_graph` and BGL conventions: `create_vertex` → `add_vertex`, `create_edge` → `add_edge`, `erase_edge` → `remove_edge`. The old member names were removed (no backward-compatible aliases); update call sites accordingly. - **`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`. diff --git a/docs/assets/adjacency_list_concepts.dot b/docs/assets/adjacency_list_concepts.dot index d5e974c..7b57fca 100644 --- a/docs/assets/adjacency_list_concepts.dot +++ b/docs/assets/adjacency_list_concepts.dot @@ -11,7 +11,8 @@ digraph ConceptHierarchy { node [fillcolor="#cce5ff"] // light blue - edge_c [label="edge\nsource_id · target_id"] + basic_edge_c [label="basic_edge\nsource_id · target_id\n(shared with edge_list)"] + edge_c [label="edge\nbasic_edge\n+ source · target"] vertex_c [label="vertex\nvertex_descriptor\nvertex_id · find_vertex"] hashable_vertex_id [label="hashable_vertex_id\nhash(vertex_id_t)"] @@ -50,6 +51,9 @@ digraph ConceptHierarchy { // ── Edges (arrows = "is required by" / "refines") ───────────────────────── + // Shared edge floor → adjacency-list edge refinement + basic_edge_c -> edge_c + // Primitives → ranges edge_c -> out_edge_range edge_c -> in_edge_range @@ -83,7 +87,8 @@ digraph ConceptHierarchy { mapped_vertex_range -> mapped_bidirectional_adjacency_list // ── Rank hints for cleaner layout ───────────────────────────────────────── - { rank=same; edge_c; vertex_c; hashable_vertex_id } + { rank=same; basic_edge_c; vertex_c; hashable_vertex_id } + { rank=same; edge_c } { rank=same; out_edge_range; in_edge_range; vertex_range } { rank=same; index_vertex_range; mapped_vertex_range } { rank=same; adjacency_list } diff --git a/docs/assets/adjacency_list_concepts.svg b/docs/assets/adjacency_list_concepts.svg index f6e715a..ee5fc31 100644 --- a/docs/assets/adjacency_list_concepts.svg +++ b/docs/assets/adjacency_list_concepts.svg @@ -1,246 +1,261 @@ - - - - - - -ConceptHierarchy - - - -edge_c - -edge<G,E> -source_id · target_id - - - -out_edge_range - -out_edge_range<R,G> -forward_range of edge<G,·> - - - -edge_c->out_edge_range - - - - - -in_edge_range - -in_edge_range<R,G> -forward_range of edge<G,·> - - - -edge_c->in_edge_range - - - - - -vertex_c - -vertex<G,V> -vertex_descriptor -vertex_id · find_vertex - - - -vertex_range - -vertex_range<R,G> -forward_range + sized_range -of vertex<G,·> - - - -vertex_c->vertex_range - - - - - -hashable_vertex_id - -hashable_vertex_id<G> -hash(vertex_id_t<G>) - - - -mapped_vertex_range - -mapped_vertex_range<G> -!index_vertex_range -hashable_vertex_id -find_vertex - - - -hashable_vertex_id->mapped_vertex_range - - - - - -adjacency_list - -adjacency_list<G> -vertex_range + out_edge_range - - - -out_edge_range->adjacency_list - - - - - -bidirectional_adjacency_list - -bidirectional_adjacency_list<G> -adjacency_list -+ in_edge_range - - - -in_edge_range->bidirectional_adjacency_list - - - - - -index_vertex_range - -index_vertex_range<G> -integral vid -integral storage_type - - - -vertex_range->index_vertex_range - - - - - -vertex_range->mapped_vertex_range - - -  !index_vertex_range - - - -vertex_range->adjacency_list - - - - - -index_adjacency_list - -index_adjacency_list<G> -adjacency_list -+ index_vertex_range - - - -index_vertex_range->index_adjacency_list - - - - - -index_bidirectional_adjacency_list - -index_bidirectional -_adjacency_list<G> -bidirectional -+ index_vertex_range - - - -index_vertex_range->index_bidirectional_adjacency_list - - - - - -mapped_adjacency_list - -mapped_adjacency_list<G> -adjacency_list -+ mapped_vertex_range - - - -mapped_vertex_range->mapped_adjacency_list - - - - - -mapped_bidirectional_adjacency_list - -mapped_bidirectional -_adjacency_list<G> -bidirectional -+ mapped_vertex_range - - - -mapped_vertex_range->mapped_bidirectional_adjacency_list - - - - - -ordered_vertex_edges - -ordered_vertex_edges<G> -adjacency_list -+ edges sorted by target_id - - - -adjacency_list->ordered_vertex_edges - - - - - -adjacency_list->bidirectional_adjacency_list - - - - - -adjacency_list->index_adjacency_list - - - - - -adjacency_list->mapped_adjacency_list - - - - - -bidirectional_adjacency_list->index_bidirectional_adjacency_list - - - - - -bidirectional_adjacency_list->mapped_bidirectional_adjacency_list - - - - - + + + + + + +ConceptHierarchy + + + +basic_edge_c + +basic_edge<G,E> +source_id · target_id +(shared with edge_list) + + + +edge_c + +edge<G,E> +basic_edge ++ source · target + + + +basic_edge_c->edge_c + + + + + +out_edge_range + +out_edge_range<R,G> +forward_range of edge<G,·> + + + +edge_c->out_edge_range + + + + + +in_edge_range + +in_edge_range<R,G> +forward_range of edge<G,·> + + + +edge_c->in_edge_range + + + + + +vertex_c + +vertex<G,V> +vertex_descriptor +vertex_id · find_vertex + + + +vertex_range + +vertex_range<R,G> +forward_range + sized_range +of vertex<G,·> + + + +vertex_c->vertex_range + + + + + +hashable_vertex_id + +hashable_vertex_id<G> +hash(vertex_id_t<G>) + + + +mapped_vertex_range + +mapped_vertex_range<G> +!index_vertex_range +hashable_vertex_id +find_vertex + + + +hashable_vertex_id->mapped_vertex_range + + + + + +adjacency_list + +adjacency_list<G> +vertex_range + out_edge_range + + + +out_edge_range->adjacency_list + + + + + +bidirectional_adjacency_list + +bidirectional_adjacency_list<G> +adjacency_list ++ in_edge_range + + + +in_edge_range->bidirectional_adjacency_list + + + + + +index_vertex_range + +index_vertex_range<G> +integral vid +integral storage_type + + + +vertex_range->index_vertex_range + + + + + +vertex_range->mapped_vertex_range + + +  !index_vertex_range + + + +vertex_range->adjacency_list + + + + + +index_adjacency_list + +index_adjacency_list<G> +adjacency_list ++ index_vertex_range + + + +index_vertex_range->index_adjacency_list + + + + + +index_bidirectional_adjacency_list + +index_bidirectional +_adjacency_list<G> +bidirectional ++ index_vertex_range + + + +index_vertex_range->index_bidirectional_adjacency_list + + + + + +mapped_adjacency_list + +mapped_adjacency_list<G> +adjacency_list ++ mapped_vertex_range + + + +mapped_vertex_range->mapped_adjacency_list + + + + + +mapped_bidirectional_adjacency_list + +mapped_bidirectional +_adjacency_list<G> +bidirectional ++ mapped_vertex_range + + + +mapped_vertex_range->mapped_bidirectional_adjacency_list + + + + + +ordered_vertex_edges + +ordered_vertex_edges<G> +adjacency_list ++ edges sorted by target_id + + + +adjacency_list->ordered_vertex_edges + + + + + +adjacency_list->bidirectional_adjacency_list + + + + + +adjacency_list->index_adjacency_list + + + + + +adjacency_list->mapped_adjacency_list + + + + + +bidirectional_adjacency_list->index_bidirectional_adjacency_list + + + + + +bidirectional_adjacency_list->mapped_bidirectional_adjacency_list + + + + + diff --git a/docs/assets/edge_list_concepts.dot b/docs/assets/edge_list_concepts.dot index 6372c3f..826885e 100644 --- a/docs/assets/edge_list_concepts.dot +++ b/docs/assets/edge_list_concepts.dot @@ -10,7 +10,8 @@ digraph EdgeListConceptHierarchy { node [fillcolor="#cce5ff"] // light blue - basic_sourced_edgelist [label="basic_sourced_edgelist\ninput_range\n!range-of-ranges\nsource_id · target_id\n(compatible return types)"] + basic_edge [label="basic_edge\nsource_id · target_id\n(shared with adj_list)"] + basic_sourced_edgelist [label="basic_sourced_edgelist\ninput_range\n!range-of-ranges\nbasic_edge>"] // ── Refinements ─────────────────────────────────────────────────────────── @@ -22,6 +23,7 @@ digraph EdgeListConceptHierarchy { // ── Edges ───────────────────────────────────────────────────────────────── + basic_edge -> basic_sourced_edgelist basic_sourced_edgelist -> basic_sourced_index_edgelist basic_sourced_edgelist -> has_edge_value diff --git a/docs/assets/edge_list_concepts.svg b/docs/assets/edge_list_concepts.svg index 8c62f42..451bf47 100644 --- a/docs/assets/edge_list_concepts.svg +++ b/docs/assets/edge_list_concepts.svg @@ -1,52 +1,65 @@ - - - - - - -EdgeListConceptHierarchy - - - -basic_sourced_edgelist - -basic_sourced_edgelist<EL> -input_range -!range-of-ranges -source_id · target_id -(compatible return types) - - - -basic_sourced_index_edgelist - -basic_sourced_index_edgelist<EL> -basic_sourced_edgelist -+ integral source_id -+ integral target_id - - - -basic_sourced_edgelist->basic_sourced_index_edgelist - - - - - -has_edge_value - -has_edge_value<EL> -basic_sourced_edgelist -+ edge_value(el, uv) - - - -basic_sourced_edgelist->has_edge_value - - - - - + + + + + + +EdgeListConceptHierarchy + + + +basic_edge + +basic_edge<EL,E> +source_id · target_id +(shared with adj_list) + + + +basic_sourced_edgelist + +basic_sourced_edgelist<EL> +input_range +!range-of-ranges +basic_edge<EL, range_value_t<EL>> + + + +basic_edge->basic_sourced_edgelist + + + + + +basic_sourced_index_edgelist + +basic_sourced_index_edgelist<EL> +basic_sourced_edgelist ++ integral source_id ++ integral target_id + + + +basic_sourced_edgelist->basic_sourced_index_edgelist + + + + + +has_edge_value + +has_edge_value<EL> +basic_sourced_edgelist ++ edge_value(el, uv) + + + +basic_sourced_edgelist->has_edge_value + + + + + diff --git a/docs/reference/adjacency-list-interface.md b/docs/reference/adjacency-list-interface.md index c0849cb..191a1bf 100644 --- a/docs/reference/adjacency-list-interface.md +++ b/docs/reference/adjacency-list-interface.md @@ -28,7 +28,8 @@ All concepts are in `graph::adj_list`, re-exported to `graph`. | Concept | Parameters | Description | |---------|------------|-------------| -| `edge` | Graph `G`, edge `E` | `E` is an `edge_descriptor`; `source_id(g,e)`, `source(g,e)`, `target_id(g,e)`, `target(g,e)` are valid | +| `basic_edge` | Graph `G`, edge `E` | Shared edge floor (in `namespace graph`): `source_id(g,e)` and `target_id(g,e)` are valid. Also satisfied by edge-list elements. | +| `edge` | Graph `G`, edge `E` | Refines `basic_edge` and additionally requires the `source(g,e)` and `target(g,e)` vertex descriptors. Always satisfied by adjacency-list edge descriptors. | ### Edge Range Concepts diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md index b8ccf3d..d73b82b 100644 --- a/docs/reference/concepts.md +++ b/docs/reference/concepts.md @@ -24,7 +24,7 @@ Header: `` ``` Primitives - edge · vertex · hashable_vertex_id + basic_edge · edge · vertex · hashable_vertex_id Ranges (parameterised on a range type R) out_edge_range requires edge @@ -49,17 +49,43 @@ Compound concepts ![Adjacency List Concept Hierarchy](../assets/adjacency_list_concepts.svg) -### `edge` +### `basic_edge` -An edge exposes at least a target vertex ID. +The shared edge floor used by **both** adjacency lists and edge lists. An edge +exposes a source and a target vertex ID. It lives in `namespace graph` (alongside +the shared `source_id` / `target_id` CPOs) and is the concept required by +`edge_list::basic_sourced_edgelist`. ```cpp -template -concept edge = requires(G& g, E& e) { - { target_id(g, e) } -> std::convertible_to>; +template +concept basic_edge = requires(G& g, const E& e) { + source_id(g, e); + target_id(g, e); }; ``` +> Return types are intentionally left unconstrained so the compiler reports the +> actual mismatch at the point of use (same rationale as `std::ranges::sized_range`). + +### `edge` + +The adjacency-list refinement of `basic_edge`. In addition to the source/target +IDs it requires the source and target vertex **descriptors** via the `source` and +`target` CPOs. Within an `adjacency_list` these are always available — the default +`source`/`target` resolve to `*find_vertex(g, source_id/target_id(g, e))` — so the +requirement is free for conforming graphs while keeping bare edge-list elements +(which have no vertex container) out of this concept. + +```cpp +template +concept edge = + basic_edge && + requires(G& g, const E& e) { + source(g, e); + target(g, e); + }; +``` + ### `out_edge_range` A forward range of outgoing edges adjacent to a vertex. diff --git a/docs/reference/cpo-reference.md b/docs/reference/cpo-reference.md index d5dc768..398e286 100644 --- a/docs/reference/cpo-reference.md +++ b/docs/reference/cpo-reference.md @@ -52,7 +52,7 @@ All CPOs listed below are available in `namespace graph` after | `contains_out_edge` | `(g, uid, vid)` | `bool` | O(deg) | Yes | | `contains_in_edge` | `(g, uid, vid)` | `bool` | O(deg) | Yes | | `has_edges` | `(g)` | `bool` | O(V) | Yes | -| `vertex_value` | `(g, u)` | `decltype(auto)` | O(1) | No | +| `vertex_value` | `(g, u)` / `(g, uid)` | `decltype(auto)` | O(1) | No | | `edge_value` | `(g, uv)` | `decltype(auto)` | O(1) | Yes | | `graph_value` | `(g)` | `decltype(auto)` | O(1) | No | | `partition_id` | `(g, u)` | `partition_id_t` | O(1) | No | @@ -379,16 +379,22 @@ decay or copy. ### `vertex_value(g, u)` ```cpp -auto vertex_value(G& g, vertex_t& u) -> /* decltype(auto) */; +auto vertex_value(G& g, vertex_t& u) -> /* decltype(auto) */; // by descriptor +auto vertex_value(G& g, vertex_id_t uid) -> /* decltype(auto) */; // by id (convenience) ``` Returns the user-defined value associated with vertex `u`. **No default** — the graph must provide this via member or ADL. +The `(g, uid)` overload is a convenience form that mirrors the descriptor +dispatch: a member `g.vertex_value(uid)` or ADL `vertex_value(g, uid)` taking the +id directly is preferred, and only when neither exists does it fall back to +`vertex_value(g, *find_vertex(g, uid))`. + | Property | Value | |----------|-------| | **Return type** | `decltype(auto)` — preserves by-value, by-ref, by-const-ref, and by-rvalue-ref | -| **Complexity** | O(1) | +| **Complexity** | O(1) for the descriptor form; the `uid` fallback adds `find_vertex` cost (O(1) index, O(log n)/O(1) avg mapped) | ### `edge_value(g, uv)` diff --git a/docs/reference/edge-list-interface.md b/docs/reference/edge-list-interface.md index 4946d8d..d56de7f 100644 --- a/docs/reference/edge-list-interface.md +++ b/docs/reference/edge-list-interface.md @@ -30,16 +30,23 @@ namespace graph::edge_list { ... } | Concept | Parameters | Description | |---------|------------|-------------| -| `basic_sourced_edgelist` | Edge list `EL` | `input_range` of non-nested elements; `source_id(el, uv)` and `target_id(el, uv)` are valid. Vertex ID type can be any type. | +| `basic_sourced_edgelist` | Edge list `EL` | `input_range` of non-nested elements whose element satisfies the shared `basic_edge` concept (`source_id(el, uv)` and `target_id(el, uv)` are valid). Vertex ID type can be any type. | | `basic_sourced_index_edgelist` | Edge list `EL` | Refines `basic_sourced_edgelist`; `source_id` and `target_id` return `std::integral` types. | | `has_edge_value` | Edge list `EL` | Refines `basic_sourced_edgelist`; `edge_value(el, uv)` is valid. | +> **Shared edge floor:** `basic_sourced_edgelist` rests on the same +> `graph::basic_edge>` concept that the adjacency-list +> `edge` concept refines, tying both abstract data types together at the edge +> level. The adjacency-list `edge` additionally requires the `source`/`target` +> vertex descriptors, which edge-list elements do not provide. + ### Concept Hierarchy ``` -basic_sourced_edgelist -├── basic_sourced_index_edgelist (+ integral vertex IDs) -└── has_edge_value (+ edge_value(el, uv)) +basic_edge (shared with adj_list: source_id · target_id) +└── basic_sourced_edgelist + ├── basic_sourced_index_edgelist (+ integral vertex IDs) + └── has_edge_value (+ edge_value(el, uv)) ``` --- diff --git a/docs/user-guide/adjacency-lists.md b/docs/user-guide/adjacency-lists.md index 42e7a3b..f6fabd6 100644 --- a/docs/user-guide/adjacency-lists.md +++ b/docs/user-guide/adjacency-lists.md @@ -49,7 +49,8 @@ graph-v3 defines 9 concepts in `graph::adj_list` (re-exported into `graph::`). | Concept | Description | |---------|-------------| -| `edge` | Edge descriptor with `source_id`, `source`, `target_id`, and `target` | +| `basic_edge` | Shared edge floor: `source_id` and `target_id` (also satisfied by edge-list elements) | +| `edge` | Refines `basic_edge`; additionally provides the `source` and `target` vertex descriptors | | `out_edge_range` | Forward range whose elements satisfy `edge` | ### Vertex concepts diff --git a/include/graph/adj_list/adjacency_list_concepts.hpp b/include/graph/adj_list/adjacency_list_concepts.hpp index a049cf1..96f564e 100644 --- a/include/graph/adj_list/adjacency_list_concepts.hpp +++ b/include/graph/adj_list/adjacency_list_concepts.hpp @@ -22,31 +22,34 @@ namespace graph::adj_list { /** * @brief Concept for edge types * - * An edge is any type for which source_id(g, e) and target_id(g, e) are valid expressions. - * This includes adj_list edge_descriptors, edge_list descriptors, edge_data aggregates, - * tuple/pair representations, and any user-defined type with appropriate CPO support. + * An edge refines the shared graph::basic_edge concept (which requires source_id(g, e) + * and target_id(g, e)) by additionally requiring access to the source and target vertex + * descriptors via source(g, e) and target(g, e). + * + * Within an adjacency_list these descriptors are always available: out_edges(g, u) and + * in_edges(g, u) always yield edge_descriptors, and adjacency_list implies a vertex_range + * (hence find_vertex), so the default source/target CPOs resolve them via + * *find_vertex(g, source_id/target_id(g, e)). The descriptor requirement is therefore free + * for conforming graphs while keeping edge_list elements (which have no vertex container) + * out of this concept — those satisfy only graph::basic_edge. * * Requirements: - * - source_id(g, e) must be valid (returns source vertex ID) - * - target_id(g, e) must be valid (returns target vertex ID) + * - graph::basic_edge (source_id(g, e) and target_id(g, e) are valid) + * - source(g, e) must be valid (returns the source vertex descriptor) + * - target(g, e) must be valid (returns the target vertex descriptor) * * Note: Return types are not constrained to allow better compiler error messages. - * Algorithms that additionally need vertex descriptors (source(g,e) / target(g,e)) - * should add those requirements explicitly beyond this concept. * * Examples: * - adj_list edge_descriptor - * - edge_list edge_descriptor - * - edge_data - * - std::tuple (source=get<0>, target=get<1>) * - * @tparam G Graph (or edge-list container) type + * @tparam G Graph type * @tparam E Edge type */ template -concept edge = requires(G& g, const E& e) { - source_id(g, e); - target_id(g, e); +concept edge = graph::basic_edge && requires(G& g, const E& e) { + source(g, e); + target(g, e); }; // ============================================================================= diff --git a/include/graph/adj_list/detail/graph_cpo.hpp b/include/graph/adj_list/detail/graph_cpo.hpp index beb36e5..d2944e5 100644 --- a/include/graph/adj_list/detail/graph_cpo.hpp +++ b/include/graph/adj_list/detail/graph_cpo.hpp @@ -2992,6 +2992,9 @@ namespace _cpo_impls { namespace _vertex_value { enum class _St { _none, _member, _adl, _default }; + // Use the public CPO instance for the uid convenience overload + using _cpo_instances::find_vertex; + // Check for g.vertex_value(u) member function // Note: Uses G (not G&) to preserve const qualification template @@ -3026,11 +3029,53 @@ namespace _cpo_impls { } } + // ----------------------------------------------------------------------- + // uid convenience overload dispatch + // + // Mirrors the descriptor dispatch above: a member g.vertex_value(uid) or + // ADL vertex_value(g, uid) that takes the id directly is preferred, and only + // when neither exists do we fall back to vertex_value(g, *find_vertex(g, uid)). + // ----------------------------------------------------------------------- + enum class _St_uid { _none, _member, _adl, _find_vertex }; + + // Can we fall back to vertex_value(g, *find_vertex(g, uid))? Requires a valid + // find_vertex(g, uid) and that vertex_value resolves for the resulting descriptor. + template + concept _has_find_vertex_default = + requires(G g, const VId& uid) { { find_vertex(g, uid) }; } && + (_has_member>> || + _has_adl>> || + _has_default>>); + + template + [[nodiscard]] consteval _Choice_t<_St_uid> _Choose_uid() noexcept { + if constexpr (_has_member) { + return {_St_uid::_member, noexcept(std::declval().vertex_value(std::declval()))}; + } else if constexpr (_has_adl) { + return {_St_uid::_adl, noexcept(vertex_value(std::declval(), std::declval()))}; + } else if constexpr (_has_find_vertex_default) { + return {_St_uid::_find_vertex, + noexcept(*find_vertex(std::declval(), std::declval())) && + _Choose>()._No_throw}; + } else { + return {_St_uid::_none, false}; + } + } + + // The uid overload participates when uid is not itself a vertex descriptor and + // at least one of member / ADL / find_vertex-default dispatch is available. + template + concept _has_uid = !vertex_descriptor_type> && + _Choose_uid, std::remove_cvref_t>()._Strategy != _St_uid::_none; + class _fn { private: template static constexpr _Choice_t<_St> _Choice = _Choose, std::remove_cvref_t>(); + template + static constexpr _Choice_t<_St_uid> _Choice_uid = _Choose_uid, std::remove_cvref_t>(); + public: /** * @brief Get the user-defined value associated with a vertex @@ -3078,6 +3123,40 @@ namespace _cpo_impls { return u.inner_value(std::forward(g)); } } + + /** + * @brief Get the user-defined value associated with a vertex (by ID) + * + * Convenience overload mirroring the descriptor dispatch: + * 1. g.vertex_value(uid) - Member function taking the id (highest priority) + * 2. vertex_value(g, uid) - ADL taking the id (high priority) + * 3. vertex_value(g, *find_vertex(g, uid)) - find_vertex fallback (lowest priority) + * + * The fallback complexity matches find_vertex (O(1) for random-access, + * O(log n)/O(1) average for associative). + * + * @tparam G Graph type + * @tparam VId Vertex ID type (typically vertex_id_t) + * @param g Graph container + * @param uid Vertex ID + * @return Exactly the type returned by the resolved dispatch path (decltype(auto)). + */ + template + requires _has_uid, std::remove_cvref_t> + [[nodiscard]] constexpr decltype(auto) operator()(G&& g, const VId& uid) const + noexcept(_Choice_uid, std::remove_cvref_t>._No_throw) + { + using _G = std::remove_cvref_t; + using _VId = std::remove_cvref_t; + + if constexpr (_Choice_uid<_G, _VId>._Strategy == _St_uid::_member) { + return g.vertex_value(uid); + } else if constexpr (_Choice_uid<_G, _VId>._Strategy == _St_uid::_adl) { + return vertex_value(g, uid); + } else if constexpr (_Choice_uid<_G, _VId>._Strategy == _St_uid::_find_vertex) { + return (*this)(std::forward(g), *find_vertex(std::forward(g), uid)); + } + } }; } // namespace _vertex_value diff --git a/include/graph/detail/edge_cpo.hpp b/include/graph/detail/edge_cpo.hpp index 858be9c..d38e9ba 100644 --- a/include/graph/detail/edge_cpo.hpp +++ b/include/graph/detail/edge_cpo.hpp @@ -581,4 +581,36 @@ inline namespace _cpo_instances { } // namespace _cpo_instances +// ============================================================================= +// Shared edge concept +// ============================================================================= + +/** + * @brief Base edge concept shared by adjacency lists and edge lists. + * + * A basic_edge is any type for which source_id(g, e) and target_id(g, e) are + * valid expressions, where g is the owning graph or edge-list container. This is + * the minimal requirement that ties an edge to both the adjacency-list and the + * edge-list abstract data types: it depends only on the shared source_id / target_id + * CPOs and makes no assumption about vertex storage. + * + * It is satisfied by adj_list edge_descriptors, edge_list descriptors, edge_data + * aggregates, tuple/pair representations, and any user-defined type with appropriate + * source_id / target_id CPO support. + * + * Note: Return types are not constrained to allow better compiler error messages. + * + * The adjacency-list refinement adj_list::edge adds source(g, e) / target(g, e) on + * top of basic_edge; the edge_list::basic_sourced_edgelist concept requires + * basic_edge of its element type. + * + * @tparam G Graph (or edge-list container) type + * @tparam E Edge type + */ +template +concept basic_edge = requires(G& g, const E& e) { + source_id(g, e); + target_id(g, e); +}; + } // namespace graph diff --git a/include/graph/edge_list/edge_list.hpp b/include/graph/edge_list/edge_list.hpp index 2ad7106..b95bedf 100644 --- a/include/graph/edge_list/edge_list.hpp +++ b/include/graph/edge_list/edge_list.hpp @@ -88,12 +88,9 @@ namespace edge_list { // basic_sourced_edgelist: Supports ANY vertex ID type (int, string, custom types, etc.) template // For exposition only concept basic_sourced_edgelist = - std::ranges::input_range && // - !std::ranges::range> && // distinguish from adjacency list - requires(EL& el, std::ranges::range_value_t uv) { - { graph::source_id(el, uv) }; - { graph::target_id(el, uv) } -> std::convertible_to; - }; + std::ranges::input_range && // + !std::ranges::range> && // distinguish from adjacency list + graph::basic_edge>; // shared edge floor: source_id + target_id // basic_sourced_index_edgelist: Requires INTEGRAL vertex IDs (int, size_t, etc.) template // For exposition only diff --git a/tests/adj_list/cpo/test_vertex_value_cpo.cpp b/tests/adj_list/cpo/test_vertex_value_cpo.cpp index 9a8ede0..8f1fe8f 100644 --- a/tests/adj_list/cpo/test_vertex_value_cpo.cpp +++ b/tests/adj_list/cpo/test_vertex_value_cpo.cpp @@ -48,6 +48,50 @@ TEST_CASE("vertex_value - vector of vertex data structures", "[vertex_value][def REQUIRE(vertex_value(g, v2).weight == 30); } +TEST_CASE("vertex_value - by vertex id (uid overload)", "[vertex_value][uid]") { + GraphWithVertexData g = {{"Alice", 10}, {"Bob", 20}, {"Charlie", 30}}; + + // vertex_value(g, uid) resolves to vertex_value(g, *find_vertex(g, uid)) + REQUIRE(vertex_value(g, vertex_id_t{0}).name == "Alice"); + REQUIRE(vertex_value(g, vertex_id_t{1}).weight == 20); + REQUIRE(vertex_value(g, vertex_id_t{2}).name == "Charlie"); + + // The uid overload aliases the descriptor overload, including mutation. + vertex_value(g, vertex_id_t{0}).weight = 99; + REQUIRE(g[0].weight == 99); +} + +// Graph that provides a member vertex_value(uid) taking the id directly. +// The uid overload must prefer this over the find_vertex fallback. +struct VertexGraphWithUidMember { + std::vector data; + + // id-taking member (returns a sentinel-transformed value to prove it was used) + int vertex_value(std::size_t uid) const { return data[uid] + 1000; } + + // descriptor-taking member (different value, to detect which path ran) + int vertex_value(const vertex_descriptor::iterator>& u) const { return data[u.value()]; } + + auto begin() { return data.begin(); } + auto end() { return data.end(); } + auto begin() const { return data.begin(); } + auto end() const { return data.end(); } +}; + +TEST_CASE("vertex_value - uid overload prefers id-taking member over find_vertex", + "[vertex_value][uid][member]") { + VertexGraphWithUidMember g; + g.data = {10, 20, 30}; + + // uid overload must call vertex_value(uid) member -> data[uid] + 1000 + REQUIRE(vertex_value(g, std::size_t{0}) == 1010); + REQUIRE(vertex_value(g, std::size_t{2}) == 1030); + + // descriptor overload still calls the descriptor member -> data[index] + auto v1 = vertex_descriptor(1); + REQUIRE(vertex_value(g, v1) == 20); +} + TEST_CASE("vertex_value - modify vertex data", "[vertex_value][default][modify]") { GraphWithVertexData g = {{"Alice", 10}, {"Bob", 20}}; diff --git a/tests/edge_list/test_edge_list_concepts.cpp b/tests/edge_list/test_edge_list_concepts.cpp index 97a6ed6..2b1a813 100644 --- a/tests/edge_list/test_edge_list_concepts.cpp +++ b/tests/edge_list/test_edge_list_concepts.cpp @@ -161,28 +161,35 @@ TEST_CASE("has_edge_value runtime behavior", "[edge_list][runtime]") { } // ============================================================================= -// Interop with adj_list::edge concept (I.2) +// Interop with the shared graph::basic_edge concept (I.2) // ============================================================================= -TEST_CASE("edge_list types satisfy adj_list::edge concept", "[edge_list][concepts][interop]") { - // After dropping the is_edge_descriptor_v gate, any type whose elements - // support source_id(g,e) and target_id(g,e) satisfies edge. +TEST_CASE("edge_list types satisfy basic_edge but not adj_list::edge", "[edge_list][concepts][interop]") { + // Edge-list elements support the shared source_id(g,e)/target_id(g,e) CPOs, so + // they satisfy the shared graph::basic_edge floor. They do NOT satisfy the + // adjacency-list refinement adj_list::edge, which additionally requires the + // source(g,e)/target(g,e) vertex-descriptor CPOs (edge lists have no vertex + // container to resolve descriptors against). // tuple using tuple_el = std::vector>; - STATIC_REQUIRE(adj_list::edge>); + STATIC_REQUIRE(graph::basic_edge>); + STATIC_REQUIRE_FALSE(adj_list::edge>); // pair using pair_el = std::vector>; - STATIC_REQUIRE(adj_list::edge>); + STATIC_REQUIRE(graph::basic_edge>); + STATIC_REQUIRE_FALSE(adj_list::edge>); // edge_data with value using ed_type = graph::edge_data; using ed_el = std::vector; - STATIC_REQUIRE(adj_list::edge); + STATIC_REQUIRE(graph::basic_edge); + STATIC_REQUIRE_FALSE(adj_list::edge); // edge_list::edge_descriptor using desc_type = edge_list::edge_descriptor; using desc_el = std::vector; - STATIC_REQUIRE(adj_list::edge); + STATIC_REQUIRE(graph::basic_edge); + STATIC_REQUIRE_FALSE(adj_list::edge); }