Writing Algorithms for BGL
BGL algorithms are template functions that accept graphs and property maps as parameters. This design means your algorithm works with any graph type and any storage strategy, without knowing the concrete types at compile time.
The pattern
Every BGL algorithm follows the same structure:
-
Accept the graph as a template parameter constrained by a graph concept (e.g., VertexListGraph, IncidenceGraph).
-
Accept data (weights, distances, colors) as property map parameters.
-
Access data exclusively through
get(map, key)andput(map, key, value).
The algorithm never knows whether the data lives inside the graph, in a
std::vector, in a std::map, or is computed on the fly. It only knows
the property map interface.
Example: edge relaxation
The relax() function used inside Dijkstra’s shortest paths illustrates
this pattern. It needs two properties: edge weights and vertex distances.
Both are accepted as template parameters and accessed through get() and
put().
template <class Edge, class Graph,
class WeightPropertyMap,
class DistancePropertyMap>
bool relax(Edge e, const Graph& g,
WeightPropertyMap weight,
DistancePropertyMap distance)
{
auto u = source(e, g);
auto v = target(e, g);
if (get(distance, u) + get(weight, e) < get(distance, v)) {
put(distance, v, get(distance, u) + get(weight, e));
return true;
}
return false;
}
This function works with any combination of graph type and storage:
-
weightcould be a bundled property (get(&Road::km, g)), an external vector, or a computed map. -
distancecould be astd::vectorwrapped withmake_iterator_property_map, or avector_property_map.
The caller decides. The algorithm does not care.
Constraining your algorithm
Use graph concepts to document and enforce what your algorithm requires:
| Your algorithm needs | Require this concept |
|---|---|
Iterate over all vertices |
|
Traverse out-edges of a vertex |
|
Traverse both out-edges and in-edges |
|
Check if an edge exists between two vertices |
|
Add or remove vertices and edges |
For property maps, use static_assert to check the category:
static_assert(
std::is_convertible<
typename property_traits<DistanceMap>::category,
read_write_property_map_tag>::value,
"distance_map must be a ReadWrite property map");
This gives the caller a clear error if they pass the wrong map type.
Writing a custom property map
Implementing your own property map requires overloading get() and put()
(and optionally operator[] for Lvalue maps), plus providing
property_traits typedefs. Here is a simplified iterator_property_map:
#include <boost/graph/adjacency_list.hpp>
#include <boost/property_map/property_map.hpp>
#include <iostream>
#include <iterator>
#include <vector>
// A simplified iterator_property_map to show how custom property maps work.
// Wraps a random access iterator and an ID map (descriptor -> integer offset).
template <class Iterator, class IDMap>
class iterator_map {
public:
using key_type = typename boost::property_traits<IDMap>::key_type;
using value_type = typename std::iterator_traits<Iterator>::value_type;
using reference = typename std::iterator_traits<Iterator>::reference;
using category = boost::lvalue_property_map_tag;
iterator_map(Iterator i = Iterator(), const IDMap& id = IDMap())
: m_iter(i), m_id(id) {}
Iterator m_iter;
IDMap m_id;
};
// get(): read a value
template <class Iter, class ID>
typename std::iterator_traits<Iter>::value_type
get(const iterator_map<Iter, ID>& m,
typename boost::property_traits<ID>::key_type key)
{
return m.m_iter[get(m.m_id, key)];
}
// put(): write a value
template <class Iter, class ID>
void put(const iterator_map<Iter, ID>& m,
typename boost::property_traits<ID>::key_type key,
const typename std::iterator_traits<Iter>::value_type& value)
{
m.m_iter[get(m.m_id, key)] = value;
}
int main() {
using namespace boost;
using Graph = adjacency_list<vecS, vecS, directedS>;
Graph g(4);
add_edge(0, 1, g);
add_edge(1, 2, g);
add_edge(2, 3, g);
// Use our custom property map to store labels
std::vector<std::string> labels(num_vertices(g));
auto index = get(vertex_index, g);
iterator_map<std::vector<std::string>::iterator,
decltype(index)> label_map(labels.begin(), index);
// Write through the property map
put(label_map, vertex(0, g), "A");
put(label_map, vertex(1, g), "B");
put(label_map, vertex(2, g), "C");
put(label_map, vertex(3, g), "D");
// Read through the property map
for (auto vi = vertices(g).first; vi != vertices(g).second; ++vi) {
std::cout << "vertex " << *vi << " = " << get(label_map, *vi) << "\n";
}
}
vertex 0 = A
vertex 1 = B
vertex 2 = C
vertex 3 = D
The key insight: the IDMap converts opaque descriptors to integer offsets,
and the iterator provides contiguous storage at those offsets. The three
functions (get, put, and optionally a reference-returning accessor) are
all that algorithms need.
How raw pointers become property maps
The same mechanism is how raw C arrays work as property maps. The library
provides a property_traits specialization for pointer types:
namespace boost {
template <class T>
struct property_traits<T*> {
using value_type = T;
using key_type = std::ptrdiff_t;
using category = lvalue_property_map_tag;
};
template <class T>
void put(T* pa, std::ptrdiff_t key, const T& value) { pa[key] = value; }
template <class T>
const T& get(const T* pa, std::ptrdiff_t key) { return pa[key]; }
}
This is why int dist[N] can be passed directly as a distance map when
VertexList=vecS (vertex descriptors are integers). The same pattern applies
to std::vector::iterator, which may be a pointer.