Customization points¶
Every accessor in melon — melon::vertices, melon::out_arcs, melon::arc_target, … — is a customization point object (CPO): a constexpr function object in namespace melon, not an ordinary function. Three consequences:
- Two ways to provide one. A CPO accepts a member function (
g.out_arcs(v)) or a free function found by ADL (out_arcs(g, v)). The member is preferred. You never specialize anything insidenamespace melon. - They cannot be hijacked by ADL at the call site.
vertices(g)inside ausing namespace melon;scope resolves to the CPO, which then does the lookup itself. - Several of them fall back. When a graph does not provide a function directly, the CPO synthesizes it from what the graph does provide — which is why a structure with three members is already a
graph. - They all return by value. Every range-returning CPO decay-copies, so
vertices_range_t<G>,arcs_range_t<G>and friends are never reference types. Return a view, not a reference to a container: if your graph stores its vertices in astd::vector, returnstd::views::all(_vertices)— astd::ranges::ref_view, which copies nothing. Returningconst std::vector<vertex> &would be copied by the CPO on every call. The same rule is whyarc_targets_map()returns amapping_ref_viewrather than aconst static_map &. - They demand an lvalue graph. A range- or closure-returning CPO rejects a temporary at compile time:
melon::arcs_entries(build_graph())would hand back a view into an object that dies at the end of the expression, exactly asstd::ranges::beginrefuses rvalue containers. Bind the graph to a name first. The one exception is a borrowed graph providing the protocol itself — its handed-out ranges outlive the object, so a temporary is admitted there.
The full table¶
✓ = must be provided directly (member or ADL) if you want it. Everything else lists what it is derived from when absent.
Vertices and arcs¶
| CPO | Fallback when not provided |
|---|---|
vertices(g) |
— (primitive) |
num_vertices(g) |
std::ranges::size(vertices(g)) when that range is sized |
arcs(g) |
join of out_arcs over all vertices, or of in_arcs — see below |
num_arcs(g) |
std::ranges::size(arcs(g)) when that range is sized |
arcs_entries(g) |
arcs + arc_source + arc_target, or a join of the out- or in-incidences |
Incidence and adjacency¶
| CPO | Fallback when not provided |
|---|---|
out_arcs(g, v) |
— |
in_arcs(g, v) |
— |
arc_source(g, a) |
— |
arc_target(g, a) |
— |
out_degree(g, v) |
std::ranges::size(out_arcs(g, v)) when sized |
in_degree(g, v) |
std::ranges::size(in_arcs(g, v)) when sized |
out_neighbors(g, v) |
out_arcs(g, v) transformed by arc_target |
in_neighbors(g, v) |
in_arcs(g, v) transformed by arc_source |
arc_sources_map(g) |
maps::function over arc_source |
arc_targets_map(g) |
maps::function over arc_target |
Data¶
| CPO | Fallback |
|---|---|
create_vertex_map<T, Role = default_role>(g) / create_vertex_map<T, Role>(g, d) |
a one-parameter create_vertex_map<T> answers every role |
create_arc_map<T, Role = default_role>(g) / create_arc_map<T, Role>(g, d) |
a one-parameter create_arc_map<T> answers every role |
Both overloads must be provided, and the result must model output_mapping_of<vertex_t<G>, T> (respectively arc_t<G>). The second template parameter is the request's role. A factory takes either exactly one template parameter, the value type — what every container declares, answering every role with its standard map — or exactly two, <T, Role = default_role>, to answer per role; the two-parameter shape is probed first, so a role-aware member must keep Role defaulted for the roleless spelling create_vertex_map<T>(g) to keep working. Every melon view forwards the role it receives.
Mutation¶
None of these has a fallback; providing one is what makes the corresponding concept true.
| CPO | Concept |
|---|---|
create_vertex(g) |
has_vertex_creation |
remove_vertex(g, v), is_valid_vertex(g, v) |
has_vertex_removal; is_valid_vertex alone satisfies has_is_valid_vertex |
create_arc(g, u, v) |
has_arc_creation |
remove_arc(g, a), is_valid_arc(g, a) |
has_arc_removal; is_valid_arc alone satisfies has_is_valid_arc |
change_arc_source(g, a, s) |
has_change_arc_source |
change_arc_target(g, a, t) |
has_change_arc_target |
Undirected¶
| CPO | Fallback |
|---|---|
edges(g) |
— |
num_edges(g) |
std::ranges::size(edges(g)) when sized |
edge_endpoints(g, e) |
— |
incidence(g, v) |
— |
degree(g, v) |
std::ranges::size(incidence(g, v)) when sized |
create_edge_map<T, Role = default_role>(g) / create_edge_map<T, Role>(g, d) |
a one-parameter create_edge_map<T> answers every role |
Choosing between fallbacks¶
arcs and arcs_entries may have several routes available at once, and the CPO picks by range category, preferring the stronger one. The internal detail::range_rank ranks contiguous over random-access over bidirectional over forward over input.
For arcs_entries the priority is:
- a member or ADL
arcs_entries— always wins, provided its entries have the documented shape: tuple-likes of size 2 pairing the arc with a tuple-like(source, target)pair. A member with any other shape is not the protocol — the waystd::ranges::beginignores a memberbegin()that returns a non-iterator — and the CPO moves on to the routes below; - listing
arcsand pairing each witharc_source/arc_target, when thearcsrange ranks at least as high as the incidence ranges; - joining the out-incidences, when they rank above the in-incidences;
- joining the in-incidences otherwise.
For arcs, when both incidence directions are available, whichever range ranks higher is joined.
The practical reading: a structure that stores an explicit arc list keeps its random-access arc iteration, and one that stores adjacency lists gets a correct forward-only arc range rather than nothing. If you can produce entries more directly than either route, define arcs_entries and the choice is skipped.
noexcept propagation¶
Each CPO computes its own noexcept from the expression it will actually evaluate. A noexcept member accessor yields a noexcept CPO call; the synthesized fallbacks that build range adaptors are conservatively not noexcept. Nothing is asserted about your accessors — mark them noexcept when they are, and it propagates.
Writing an adapter¶
namespace their_lib {
struct their_graph { ... };
inline auto vertices(const their_graph & g) noexcept { ... }
inline auto out_arcs(const their_graph & g, unsigned int v) noexcept { ... }
inline unsigned int arc_target(const their_graph & g, unsigned int a) noexcept { ... }
template <typename T> auto create_vertex_map(const their_graph & g) { ... }
template <typename T> auto create_vertex_map(const their_graph & g, const T & d) { ... }
} // namespace their_lib
Nothing is added to namespace melon; ADL from the argument type finds them. The map factories are called with an explicit template argument — create_vertex_map<T>(g), or create_vertex_map<T, Role>(g) for a two-parameter factory — and that works because on melon's side create_vertex_map is a variable template wrapping a CPO object, not a function template: the melon name is invisible to ADL, so the ADL probe inside the CPO can only ever find your create_vertex_map.
See Bringing your own graph for complete, compiling examples and the rules the ranges must respect. graph_ref_view, which every algorithm wraps its argument in, forwards every read-only accessor in the tables above — arcs_entries when the wrapped graph provides its own; otherwise the CPO fallback synthesizes it — so a type stays a graph once wrapped whichever protocol it provides. It does not forward the mutating CPOs — a view is read-only by construction — but it does forward is_valid_vertex and is_valid_arc: those are questions, not mutations, and the standalone concepts has_is_valid_vertex<G> / has_is_valid_arc<G> name a graph that answers them.