Can this be achieved using boost::graph?
The answer is nearly always "yes" with BGL. It's one of the most profoundly generic library designs in existence.
To my surprise, something new appeared in the type-hierarchy for adjacency_list
. Apparently these days there's a named_graph
mixin (actually maybe_name_graph
) which uses traits on the vertex bundle to detect an "internal name".
This means you can get close. Though the vertex descriptor will not become your id, you can have O(1) lookup. And the interface has some conveniences, so you can write:
add_edge(1111, 2222, g);
add_edge(2222, 1111, g);
Notes:
- the lookup is amortized O(1) because the name-vertex lookup is based on an unordered (hash) index
- you run into problems (e.g. ambiguous overloads for
add_edge
) if you make the id
type accidentally the same as the vertex_descriptor
type (or if your argument have equally "far" conversions to either).
- as far as I can tell, the internal name property is not automatically picked up as the
vertex_index_t
nor vertex_name_t
property.
Step #1: The Graph
struct Vertex {
size_t id;
std::string other = "default-constructed";
};
using Graph =
boost::adjacency_list<boost::vecS, boost::listS, boost::directedS, Vertex>;
So far no surprises. I
- opted to add a second member
other
just to show when/how it gets default constructed
- opted
listS
because it
- a. offers reference/descriptor stability (except for removed vertices)
- b. leads to opaque
vertex_descriptor
(void*
) which does not conflict with the id type (size_t
) in overload resolution
Step #2: Name Traits
Next we teach BGL about our Internal Vertex Name.
This is purely parameterized by Vertex
bundle, so keep in mind that different graphs using the same bundle would use the same name traits.
// traits
template <> struct boost::graph::internal_vertex_name<Vertex> {
struct type {
using result_type = size_t;
result_type const& operator()(Vertex const& bundle) const {
return bundle.id;
}
};
};
template <> struct boost::graph::internal_vertex_constructor<Vertex> {
struct type {
private:
using extract_name_type = typename internal_vertex_name<Vertex>::type;
using vertex_name_type = typename remove_cv<typename remove_reference<
typename extract_name_type::result_type>::type>::type;
public:
using argument_type = vertex_name_type;
using result_type = Vertex;
result_type operator()(const vertex_name_type& id) const {
return {id};
}
};
};
Note
We could of course hard-code the knowns in the second specialization:
template <> struct boost::graph::internal_vertex_constructor<Vertex> {
struct type {
Vertex operator()(size_t id) const { return {id}; }
};
};
It is very important to return the id by reference. Failing to do so leads to UB with no diagnostics from the library/compiler
Step #3 Adding/Finding Vertices
Now you can add vertices. Either by "name" (your id
):
auto x = add_vertex(1111, g); // by id
Or the old-fashioned way you anticipated in the question:
add_vertex(Vertex{2222, "twotwotwotwo"}, g); // or with full bundle
Duplicate additions have no effect:
assert(add_vertex(1111, g) == x);
Different lookups exist. The vertex_by_property
returns a optional<vertex_descriptor>
given a vertex bundle.
assert(x == *g.vertex_by_property(g[x]));
It does so by extracting the "internal name" from the bundle passed, so there is no need for the bundle to contain any other state outside the id:
assert(x == *g.vertex_by_property(Vertex{1111}));
Although it feels like an implementation detail, the actual multi_index_container
implementing the name -> descriptor index is also exposed:
assert(x == *g.named_vertices.find(1111));
Step #4 Adding Edges
add_edge(1111, 2222, g);
add_edge(2222, 1111, g);
That borrows a page from your previous question :)
Obviously, you can still add edges by vertex descriptors.
Step #5 Other Operations
Borrowing more pages from that previous answer:
print_graph(g, get(&Vertex::id, g), std::cout << "---\n");
print_graph(g, get(&Vertex::other, g), std::cout << "---\n");
std::cout << "---\n";
for (auto *v : boost::make_iterator_range(vertices(g))) {
auto const& [id, other] = g[v];
std::cout << id << " " << std::quoted(other) << "\n";
}
if (auto v = g.vertex_by_property({1111})) {
std::cout << "==== removing " << g[*v].id << "\n";
clear_vertex(*v, g); // clear edges
remove_vertex(*v, g); // remove the vertex
}
print_graph(g, get(&Vertex::id, g), std::cout << "---\n");
print_graph(g, get(&Vertex::other, g), std::cout << "---\n");
Full Demo
Live On Coliru
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graph_utility.hpp>
#include <iostream>
#include <iomanip>
struct Vertex {
size_t id;
std::string other = "default-constructed";
};
using Graph =
boost::adjacency_list<boost::vecS, boost::listS, boost::directedS, Vertex>;
// traits
template <> struct boost::graph::internal_vertex_name<Vertex> {
struct type {
using result_type = size_t;
result_type const& operator()(Vertex const& bundle) const {
return bundle.id;
}
};
};
template <> struct boost::graph::internal_vertex_constructor<Vertex> {
struct type {
private:
using extractor = typename internal_vertex_name<Vertex>::type;
using name_t = std::decay_t<typename extractor::result_type>;
public:
using argument_type = name_t;
using result_type = Vertex;
result_type operator()(const name_t& id) const { return {id}; }
};
};
int main() {
Graph g;
{
auto x = add_vertex(1111, g); // by id
add_vertex(Vertex{2222, "twotwotwotwo"}, g); // or with full bundle
// duplicate additions have no effect
assert(add_vertex(1111, g) == x);
// different lookups exist
assert(x == *g.named_vertices.find(1111));
assert(x == *g.vertex_by_property(Vertex{1111}));
assert(x == *g.vertex_by_property(g[x]));
}
add_edge(1111, 2222, g);
add_edge(2222, 1111, g);
print_graph(g, get(&Vertex::id, g), std::cout << "---\n");
print_graph(g, get(&Vertex::other, g), std::cout << "---\n");
std::cout << "---\n";
for (auto *v : boost::make_iterator_range(vertices(g))) {
auto const& [id, other] = g[v];
std::cout << id << " " << std::quoted(other) << "\n";
}
if (auto v = g.vertex_by_property({1111})) {
std::cout << "==== removing " << g[*v].id << "\n";
clear_vertex(*v, g); // clear edges
remove_vertex(*v, g); // remove the vertex
}
print_graph(g, get(&Vertex::id, g), std::cout << "---\n");
print_graph(g, get(&Vertex::other, g), std::cout << "---\n");
}
Prints
---
1111 --> 2222
2222 --> 1111
---
default-constructed --> twotwotwotwo
twotwotwotwo --> default-constructed
---
1111 "default-constructed"
2222 "twotwotwotwo"
==== removing 1111
---
2222 -->
---
twotwotwotwo -->
Sidenote:
- you cannot use an associative container selector for vertices. Specifying
hash_mapS
leads to unordered_set<void*, hash<void*>, equal_to<void*>, allocator<void*>>
as the m_vertices
type.