C++ class templates for graph construction and search
This document provides an in-depth look at the libgraph library architecture, design patterns, and implementation details for contributors and advanced users.
libgraph is built on several core principles:
template<typename State, typename Transition = double, typename StateIndexer = DefaultIndexer<State>>
class Graph;
The State
type represents vertex data and must satisfy:
// Option 1: Has public id field
struct MyState {
int64_t id; // Used by DefaultIndexer
};
// Option 2: Has GetId() method
struct MyState {
int64_t GetId() const { return unique_id; }
};
// Option 3: Custom indexer
struct CustomIndexer {
int64_t operator()(const MyState& state) const {
return state.custom_field;
}
};
The Transition
type represents edge weights/costs and must support:
// Required operations for search algorithms
bool operator<(const Transition& other) const;
bool operator>(const Transition& other) const;
Transition operator+(const Transition& other) const;
// Required for initialization
template<> struct CostTraits<MyTransition> {
static MyTransition infinity() { /* return max value */ }
};
Generates unique int64_t IDs for states:
struct StateIndexer {
int64_t operator()(const State& state) const {
// Generate unique ID from state
}
};
The library uses partial specialization and SFINAE for customization:
// Automatic detection of indexing method
template<typename T>
auto GetStateId(const T& state) -> decltype(state.id) {
return state.id;
}
template<typename T>
auto GetStateId(const T& state) -> decltype(state.GetId()) {
return state.GetId();
}
template<typename T>
auto GetStateId(const T& state) -> decltype(state.id_) {
return state.id_;
}
class Graph {
private:
std::unordered_map<int64_t, std::unique_ptr<Vertex<State, Transition>>> vertices_;
StateIndexer state_indexer_;
public:
// Vertex management
void AddVertex(const State& state);
bool RemoveVertex(const State& state);
Vertex<State, Transition>* GetVertexPtr(const State& state);
// Edge management
void AddEdge(const State& from, const State& to, const Transition& cost);
bool RemoveEdge(const State& from, const State& to);
};
template<typename State, typename Transition>
class Vertex {
State state_; // User data
std::list<Edge<State, Transition>> edges_; // Outgoing edges
int64_t id_; // Unique identifier
public:
using EdgeIterator = typename std::list<Edge<State, Transition>>::iterator;
using ConstEdgeIterator = typename std::list<Edge<State, Transition>>::const_iterator;
};
template<typename State, typename Transition>
class Edge {
Vertex<State, Transition>* dst_; // Destination vertex pointer
Transition cost_; // Edge weight/cost
int64_t id_; // Unique edge identifier
public:
const Transition& GetCost() const { return cost_; }
Vertex<State, Transition>* GetDst() const { return dst_; }
};
The library follows strict RAII (Resource Acquisition Is Initialization) principles:
class Graph {
private:
// Automatic cleanup through smart pointers
std::unordered_map<int64_t, std::unique_ptr<Vertex<State, Transition>>> vertices_;
public:
// Exception-safe vertex addition
void AddVertex(const State& state) {
auto vertex = std::make_unique<Vertex<State, Transition>>(state, state_indexer_(state));
// Strong exception safety: either succeeds completely or has no effect
auto result = vertices_.emplace(vertex->GetId(), std::move(vertex));
if (!result.second) {
throw std::invalid_argument("Vertex with this ID already exists");
}
}
};
// Vertex memory layout optimized for cache efficiency
class Vertex {
State state_; // Hot data: accessed frequently during searches
int64_t id_; // Hot data: used for indexing
std::list<Edge> edges_; // Cold data: only accessed when exploring edges
};
// Graph owns vertices exclusively
std::unique_ptr<Vertex<State, Transition>> vertex_ptr;
// Edges hold raw pointers to vertices (non-owning references)
class Edge {
Vertex<State, Transition>* dst_; // Raw pointer - graph manages lifetime
};
All search algorithms implement a common interface using CRTP (Curiously Recurring Template Pattern):
template<typename Derived>
class SearchAlgorithmBase {
public:
template<typename Graph, typename State>
static std::vector<State> Search(const Graph& graph,
const State& start,
const State& goal) {
return static_cast<Derived*>(nullptr)->SearchImpl(graph, start, goal);
}
};
class Dijkstra : public SearchAlgorithmBase<Dijkstra> {
public:
template<typename Graph, typename State>
std::vector<State> SearchImpl(const Graph& graph,
const State& start,
const State& goal) {
// Dijkstra-specific implementation
}
};
template<typename State, typename Transition = double>
class SearchContext {
private:
// Search state storage
std::unordered_map<int64_t, double> distances_;
std::unordered_map<int64_t, int64_t> predecessors_;
DynamicPriorityQueue<State, Transition> priority_queue_;
public:
// Thread-safe state management
void Reset() { /* Clear all state for reuse */ }
void PreAllocate(size_t estimated_vertices) { /* Reserve memory */ }
};
template<typename State, typename Transition, typename Compare>
class DynamicPriorityQueue {
private:
std::vector<std::pair<Transition, int64_t>> heap_; // Binary heap
std::unordered_map<int64_t, size_t> position_map_; // State ID -> heap position
Compare comparator_;
public:
void Push(const State& state, const Transition& priority);
State Pop();
void UpdatePriority(const State& state, const Transition& new_priority);
bool Empty() const;
};
// Multiple threads can safely perform concurrent searches
void MultiThreadedSearch() {
const Graph<Location> map = BuildGraph(); // Immutable after construction
std::vector<std::thread> workers;
for (int i = 0; i < num_threads; ++i) {
workers.emplace_back([&map, i]() {
SearchContext<Location> context; // Thread-local state
auto path = Dijkstra::Search(map, context, starts[i], goals[i]);
ProcessPath(path);
});
}
for (auto& t : workers) t.join();
}
Graph modifications require external synchronization:
class ThreadSafeGraph {
private:
Graph<State, Transition> graph_;
mutable std::shared_mutex mutex_;
public:
// Exclusive write access
void AddVertex(const State& state) {
std::lock_guard<std::shared_mutex> lock(mutex_);
graph_.AddVertex(state);
}
// Concurrent read access
template<typename... Args>
auto Search(Args&&... args) const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return Dijkstra::Search(graph_, std::forward<Args>(args)...);
}
};
Operation | Average Case | Worst Case | Notes |
---|---|---|---|
AddVertex | O(1) | O(1) | Hash table insertion |
RemoveVertex | O(d) | O(d) | d = vertex degree |
AddEdge | O(1) | O(1) | List insertion |
RemoveEdge | O(d) | O(d) | Linear search in edge list |
Search | O((m+n) log n) | O((m+n) log n) | Priority queue operations |
// Memory usage breakdown for Graph<State, Transition>
// Vertices: n * (sizeof(State) + sizeof(Vertex) + hash_table_overhead)
// Edges: m * (sizeof(Transition) + sizeof(Edge) + list_node_overhead)
// Total: O(n * sizeof(State) + m * sizeof(Transition))
// Edge list traversal pattern optimized for cache efficiency
for (const auto& edge : vertex->GetEdges()) {
// Sequential memory access through linked list
ProcessEdge(edge);
}
// Hash table access pattern for vertex lookup
auto* vertex = graph.GetVertexPtr(state); // Single hash lookup
Used for static polymorphism in search algorithms:
template<typename Derived>
class SearchAlgorithm {
public:
template<typename... Args>
auto Search(Args&&... args) -> decltype(static_cast<Derived*>(this)->SearchImpl(std::forward<Args>(args)...)) {
return static_cast<Derived*>(this)->SearchImpl(std::forward<Args>(args)...);
}
};
Benefits:
Cost comparison and heuristic functions:
template<typename State, typename Transition, typename Compare = std::less<Transition>>
class Dijkstra {
private:
Compare comparator_; // Strategy for cost comparison
public:
Dijkstra(Compare comp = Compare{}) : comparator_(comp) {}
};
Customization points for user types:
// Primary template
template<typename T>
struct CostTraits {
static T infinity() { return std::numeric_limits<T>::max(); }
};
// User specialization
template<>
struct CostTraits<MyCustomCost> {
static MyCustomCost infinity() { return MyCustomCost::MaxValue(); }
};
Exception-safe resource management:
class SearchContext {
private:
std::unique_ptr<ContextImpl> impl_; // RAII for internal state
public:
SearchContext() : impl_(std::make_unique<ContextImpl>()) {}
~SearchContext() = default; // Automatic cleanup
// Non-copyable, movable
SearchContext(const SearchContext&) = delete;
SearchContext(SearchContext&&) = default;
};
Requirements and best practices:
struct CustomState {
// Required: Unique identification
int64_t GetId() const { return id_; }
// Recommended: Equality comparison
bool operator==(const CustomState& other) const {
return id_ == other.id_;
}
// Optional: Hash function for unordered containers
struct Hash {
size_t operator()(const CustomState& state) const {
return std::hash<int64_t>{}(state.GetId());
}
};
private:
int64_t id_;
// User data...
};
Implementation requirements:
struct CustomCost {
// Required for search algorithms
bool operator<(const CustomCost& other) const;
bool operator>(const CustomCost& other) const;
bool operator<=(const CustomCost& other) const;
bool operator>=(const CustomCost& other) const;
bool operator==(const CustomCost& other) const;
bool operator!=(const CustomCost& other) const;
// Required for path cost accumulation
CustomCost operator+(const CustomCost& other) const;
CustomCost& operator+=(const CustomCost& other);
// Required for A* (optional for other algorithms)
CustomCost operator-(const CustomCost& other) const;
};
// Required: CostTraits specialization
namespace xmotion {
template<>
struct CostTraits<CustomCost> {
static CustomCost infinity() { return CustomCost::Max(); }
};
}
For A* algorithm:
// Function object approach
struct ManhattanDistance {
double operator()(const GridPoint& from, const GridPoint& to) const {
return std::abs(from.x - to.x) + std::abs(from.y - to.y);
}
};
// Lambda approach
auto euclidean = [](const Point& from, const Point& to) -> double {
double dx = from.x - to.x;
double dy = from.y - to.y;
return std::sqrt(dx * dx + dy * dy);
};
// Usage
auto path = AStar::Search(graph, start, goal, ManhattanDistance{});
auto path2 = AStar::Search(graph, start, goal, euclidean);
For specialized use cases:
template<typename State, typename Transition, typename Compare>
class CustomPriorityQueue {
public:
void Push(const State& state, const Transition& priority);
State Pop();
void UpdatePriority(const State& state, const Transition& new_priority);
bool Empty() const;
size_t Size() const;
};
This architecture provides a solid foundation for high-performance graph operations while maintaining flexibility and type safety through modern C++ template techniques.