Skip to content

Commit 3e59c37

Browse files
authored
Merge pull request #525 from Becheler/fix/subgraph-124-ci
fix: undefined behavior when transforming non-existing edges in a subgraph
2 parents b648c6e + 7db1f2e commit 3e59c37

4 files changed

Lines changed: 232 additions & 21 deletions

File tree

doc/modules/ROOT/pages/adaptors/subgraph.adoc

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,47 @@ template <typename Graph>
3030
class subgraph;
3131
----
3232

33-
The `subgraph` class wraps an `adjacency_list` and maintains a tree of
34-
subgraphs. The root graph owns all vertices and edges. Child subgraphs hold
35-
subsets of the root's vertices, and their edge sets are _induced_: any edge in
36-
the root whose both endpoints are in the child automatically appears in the
37-
child.
38-
39-
Each subgraph uses _local_ descriptors (0-based within that subgraph). Use
40-
`local_to_global()` and `global_to_local()` to convert between local and root
41-
descriptors.
42-
43-
Adding an edge to a child subgraph also adds it to all ancestor subgraphs.
44-
Adding a vertex to a child subgraph also adds it to all ancestors.
33+
`subgraph` represents parts of a single graph as independent graphs, without
34+
copying the data and without the parts drifting out of sync with the whole. All
35+
vertices and edges are stored once, in the root graph. Each subgraph is a
36+
lightweight view onto a subset of that storage which still behaves like a full
37+
graph, so it can be passed to any BGL algorithm.
38+
39+
[TIP]
40+
====
41+
Consider a national road network: intersections are vertices and roads are
42+
edges. An analysis might focus on one city, then one district within that city,
43+
then compare against the whole country. Rather than three separate copies kept
44+
consistent by hand, `subgraph` keeps one dataset and offers lightweight views
45+
onto its parts. The country is the root, a city is a child subgraph, and a
46+
district is a child of that city. Each view behaves like a normal graph and can
47+
be handed to an algorithm such as `dijkstra_shortest_paths` exactly like any
48+
other BGL graph.
49+
====
50+
51+
These views nest into a tree. The _root_ subgraph holds the entire graph and
52+
owns all of its vertices and edges. Each _child_ is a subset of its parent's
53+
vertices, and a child may in turn have children of its own. A child's edges are
54+
_induced_: an edge belongs to a child exactly when both of its endpoints do, so
55+
only vertices are ever added to a child, never edges directly.
56+
57+
Because the storage is shared rather than copied, the levels stay linked in both
58+
directions. A property set at one level, such as an edge weight or a vertex
59+
color, is visible from every level. Adding a vertex or edge to a child also adds
60+
it to every ancestor up to the root, so an edit made while working on one part
61+
keeps the whole hierarchy consistent.
62+
63+
Each subgraph numbers its own vertices and edges 0-based, as required by the
64+
many algorithms that assume a contiguous index space. A descriptor is therefore
65+
meaningful only within one subgraph. `local_to_global()` and `global_to_local()`
66+
translate a descriptor between a subgraph's local numbering and the root's.
67+
68+
Choosing among the graph views:
69+
70+
* `filtered_graph` for a read-only view that hides some vertices or edges.
71+
* `copy_graph` for an independent copy that does not stay linked to the original.
72+
* `subgraph` when mutability, per-region local numbering, and a parent/child
73+
hierarchy over shared storage are all needed.
4574

4675
== Template Parameters
4776

@@ -102,16 +131,49 @@ edge_descriptor global_to_local(edge_descriptor e_global) const;
102131

103132
Convert between local (subgraph-relative) and global (root) descriptors.
104133

134+
[WARNING]
135+
====
136+
These conversions have preconditions, and the two directions differ.
137+
138+
* `global_to_local` requires that the element is in this subgraph. In debug
139+
builds a violation trips `BOOST_ASSERT`; with assertions disabled it returns a
140+
default-constructed descriptor (`null_vertex()` for vertices). When the element
141+
might not be present, query membership first with `find_vertex()` or
142+
`find_edge()`.
143+
* `local_to_global` requires a valid local descriptor of this subgraph. A
144+
violation is undefined behaviour (an unchecked lookup) in all build
145+
configurations.
146+
147+
On the root subgraph every conversion is the identity, so any element is
148+
accepted.
149+
====
150+
105151
'''
106152

107153
[source,cpp]
108154
----
109155
std::pair<vertex_descriptor, bool>
110156
find_vertex(vertex_descriptor u_global) const;
157+
158+
std::pair<edge_descriptor, bool>
159+
find_edge(edge_descriptor e_global) const;
111160
----
112161

113-
Returns `(local_descriptor, true)` if the global vertex is in this subgraph,
114-
`(_, false)` otherwise.
162+
Membership queries that never assert. `find_vertex` returns
163+
`(local_descriptor, true)` if the global vertex is in this subgraph, and
164+
`(null_vertex(), false)` otherwise. `find_edge` behaves the same way for edges,
165+
returning a default-constructed `edge_descriptor` in the not-found case. These
166+
are the safe form of `global_to_local`, for the case where the element is not
167+
known in advance to be in the subgraph.
168+
169+
[NOTE]
170+
====
171+
*Descriptor identity and stability.* For an element that is present, the
172+
conversions round-trip: `global_to_local(local_to_global(x)) == x`, and the
173+
edge index is preserved. Properties live in the root's storage and are shared
174+
across the whole tree, so the same logical element carries the same property
175+
value no matter which subgraph's descriptor reaches it.
176+
====
115177

116178
=== Hierarchy Navigation
117179

include/boost/graph/subgraph.hpp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,14 @@ template < typename Graph > class subgraph
207207

208208
vertex_descriptor global_to_local(vertex_descriptor u_global) const
209209
{
210-
vertex_descriptor u_local;
211-
bool in_subgraph;
212210
if (is_root())
213211
return u_global;
212+
vertex_descriptor u_local;
213+
bool in_subgraph;
214214
boost::tie(u_local, in_subgraph) = this->find_vertex(u_global);
215-
BOOST_ASSERT(in_subgraph == true);
215+
BOOST_ASSERT_MSG(in_subgraph,
216+
"global_to_local: vertex is not in this subgraph. "
217+
"Use find_vertex() to check membership first.");
216218
return u_local;
217219
}
218220

@@ -225,10 +227,15 @@ template < typename Graph > class subgraph
225227

226228
edge_descriptor global_to_local(edge_descriptor e_global) const
227229
{
228-
return is_root() ? e_global
229-
: (*m_local_edge.find(
230-
get(get(edge_index, root().m_graph), e_global)))
231-
.second;
230+
if (is_root())
231+
return e_global;
232+
edge_descriptor e_local;
233+
bool in_subgraph;
234+
boost::tie(e_local, in_subgraph) = this->find_edge(e_global);
235+
BOOST_ASSERT_MSG(in_subgraph,
236+
"global_to_local: edge is not in this subgraph. "
237+
"Use find_edge() to check membership first.");
238+
return e_local;
232239
}
233240

234241
// Is vertex u (of the root graph) contained in this subgraph?

test/Jamfile.v2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ alias graph_test_regular :
9797
[ run subgraph_bundled.cpp ]
9898
[ run subgraph_add.cpp : $(TEST_DIR) ]
9999
[ run subgraph_props.cpp ]
100+
[ run subgraph_global_local.cpp ]
100101

101102
[ run isomorphism.cpp ]
102103
[ run adjacency_matrix_test.cpp ]

test/subgraph_global_local.cpp

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) 2026 Arnaud Becheler
2+
// Distributed under the Boost Software License, Version 1.0. (See
3+
// accompanying file LICENSE_1_0.txt or copy at
4+
// http://www.boost.org/LICENSE_1_0.txt)
5+
6+
#include <boost/core/lightweight_test.hpp>
7+
#include <boost/graph/adjacency_list.hpp>
8+
#include <boost/graph/subgraph.hpp>
9+
10+
#include <utility>
11+
12+
using namespace boost;
13+
14+
template < typename Directedness >
15+
using subgraph_of = subgraph< adjacency_list< vecS, vecS, Directedness,
16+
property< vertex_color_t, int >,
17+
property< edge_index_t, std::size_t, property< edge_weight_t, int > > > >;
18+
19+
template < typename Directedness >
20+
void test_descriptor_conversion()
21+
{
22+
using graph_t = subgraph_of< Directedness >;
23+
using vertex_t = typename graph_traits< graph_t >::vertex_descriptor;
24+
using edge_t = typename graph_traits< graph_t >::edge_descriptor;
25+
using vertex_iterator = typename graph_traits< graph_t >::vertex_iterator;
26+
using edge_iterator = typename graph_traits< graph_t >::edge_iterator;
27+
28+
// root: path 0-1-2-3, with edge indices assigned in insertion order.
29+
graph_t root(4);
30+
add_edge(0, 1, root); // edge index 0
31+
add_edge(1, 2, root); // edge index 1
32+
add_edge(2, 3, root); // edge index 2
33+
34+
// child g1 = {1, 2, 3}: induces edges (1,2) and (2,3), but not (0,1).
35+
graph_t& g1 = root.create_subgraph();
36+
add_vertex(1, g1);
37+
add_vertex(2, g1);
38+
add_vertex(3, g1);
39+
40+
// grandchild g1a = {2, 3}: induces only edge (2,3).
41+
graph_t& g1a = g1.create_subgraph();
42+
add_vertex(2, g1a);
43+
add_vertex(3, g1a);
44+
45+
// invariant: for an edge in the subgraph, global_to_local(local_to_global(e))
46+
// must equal e, and its edge_index must be preserved.
47+
edge_iterator ei, ei_end;
48+
for (boost::tie(ei, ei_end) = edges(g1); ei != ei_end; ++ei)
49+
{
50+
edge_t e_local = *ei;
51+
edge_t e_round = g1.global_to_local(g1.local_to_global(e_local));
52+
BOOST_TEST(e_round == e_local);
53+
BOOST_TEST(get(edge_index, g1, e_local) == get(edge_index, g1, e_round));
54+
}
55+
56+
// invariant: for a vertex in the subgraph, global_to_local(local_to_global(v))
57+
// must equal v, and its vertex_index must be preserved.
58+
vertex_iterator vi, vi_end;
59+
for (boost::tie(vi, vi_end) = vertices(g1); vi != vi_end; ++vi)
60+
{
61+
vertex_t v_local = *vi;
62+
vertex_t v_round = g1.global_to_local(g1.local_to_global(v_local));
63+
BOOST_TEST(v_round == v_local);
64+
BOOST_TEST(get(vertex_index, g1, v_local) == get(vertex_index, g1, v_round));
65+
}
66+
67+
// invariant: a property written through a child must be readable from the
68+
// root, because the whole tree shares one property store.
69+
edge_t some_local = *edges(g1).first;
70+
put(edge_weight, g1, some_local, 42);
71+
BOOST_TEST(get(edge_weight, root, g1.local_to_global(some_local)) == 42);
72+
73+
// invariant: find_vertex must report a present vertex as true, and an absent
74+
// one as false with a null_vertex() descriptor.
75+
BOOST_TEST(g1.find_vertex(1).second); // vertex 1 is in g1
76+
BOOST_TEST(!g1.find_vertex(0).second); // vertex 0 is not in g1
77+
BOOST_TEST(g1.find_vertex(0).first == graph_traits< graph_t >::null_vertex());
78+
79+
// invariant: find_edge must report false with a default descriptor for a
80+
// root edge that is absent from the subgraph.
81+
edge_t e01 = edge(0, 1, root).first;
82+
BOOST_TEST(get(edge_index, root, e01) == 0u);
83+
BOOST_TEST(g1.find_edge(e01).first == edge_t()); // absent -> default
84+
BOOST_TEST(!g1.find_edge(e01).second); // absent -> false
85+
86+
// invariant: edge round-trip identity must hold at any nesting depth,
87+
// not just for direct children.
88+
for (boost::tie(ei, ei_end) = edges(g1a); ei != ei_end; ++ei)
89+
{
90+
edge_t e_local = *ei;
91+
BOOST_TEST(g1a.global_to_local(g1a.local_to_global(e_local)) == e_local);
92+
}
93+
94+
// invariant: a property written through a grandchild must reach the root.
95+
edge_t g1a_local = *edges(g1a).first;
96+
put(edge_weight, g1a, g1a_local, 7);
97+
BOOST_TEST(get(edge_weight, root, g1a.local_to_global(g1a_local)) == 7);
98+
99+
// invariant: an edge present in the parent but not the grandchild must
100+
// report absent from the grandchild.
101+
edge_t e12 = edge(1, 2, root).first;
102+
BOOST_TEST(!g1a.find_edge(e12).second);
103+
}
104+
105+
// idiom: find_edge is the membership query; global_to_local is the transform to
106+
// run once membership is confirmed.
107+
template < typename Directedness >
108+
void test_membership_query_then_convert()
109+
{
110+
using graph_t = subgraph_of< Directedness >;
111+
using edge_t = typename graph_traits< graph_t >::edge_descriptor;
112+
113+
graph_t root(3);
114+
add_edge(0, 1, root); // edge index 0
115+
add_edge(1, 2, root); // edge index 1
116+
117+
// sg = {1, 2}: induces edge (1,2), but not edge (0,1).
118+
graph_t& sg = root.create_subgraph();
119+
add_vertex(1, sg);
120+
add_vertex(2, sg);
121+
122+
// Present edge: the query confirms membership, then the transform agrees
123+
// with the local descriptor the query returned.
124+
edge_t e12 = edge(1, 2, root).first;
125+
std::pair< edge_t, bool > found = sg.find_edge(e12);
126+
BOOST_TEST(found.second);
127+
BOOST_TEST(sg.global_to_local(e12) == found.first);
128+
129+
// Absent edge: the query reports it, so the transform is not run.
130+
edge_t e01 = edge(0, 1, root).first;
131+
BOOST_TEST(!sg.find_edge(e01).second);
132+
}
133+
134+
int main()
135+
{
136+
test_descriptor_conversion< directedS >();
137+
test_descriptor_conversion< bidirectionalS >();
138+
test_membership_query_then_convert< directedS >();
139+
test_membership_query_then_convert< bidirectionalS >();
140+
return boost::report_errors();
141+
}

0 commit comments

Comments
 (0)