Mappings¶
Everywhere MIP++ hands values back — solutions, dual values, reduced costs — or folds an expression against values, it speaks one protocol: map[key] reads a stored value. This page explains the layer behind that protocol — the concepts generic code should constrain on, the adaptor that lifts arbitrary storage into it, and the entity-keyed layer the solvers use.
The whole layer lives in
mapping.hpp
(solver-agnostic) and the entity_mapping part of
model_entities.hpp.
test/mapping.cpp
is the authoritative example set.
The concepts¶
mapping<Map, Key> is duck-typed: map[key] must be well-formed, nothing more. On top of it:
input_mapping<Map, Key>— readable through aconst Map;output_mapping<Map, Key>— additionally,map[key] = valueassigns through a real lvalue;contiguous_mapping<Map, Key>— additionally, integral keys and a.data()pointer, for handing the storage to a C API;- each has an
_of<Map, Key, Value>variant that also pins the mapped value type.
Constrain generic code on the weakest one that suffices — evaluate requires only input_mapping.
A subtlety worth relying on: std::map<K, V> is a mapping but is not an input_mapping, because a const std::map has no operator[]. Adaptation (below) is what makes it readable const — through .at().
Adapting storage: views::mapping_all¶
views::mapping_all(m) is to mappings what std::views::all is to ranges: it lifts anything usable as a mapping into a view with the [] protocol, and is the identity on things that already are such views.
- an lvalue is referenced — the view is a
mapping_ref_view, and the operand must outlive it, like anyref_view; - an rvalue is moved into a
mapping_owning_view; - a mapping view passes through unchanged.
Inside the view, a lookup dispatches to the first protocol the storage supports, in this order: m[key] (vectors, arrays, std::unique_ptr<T[]>), then m(key) (callables), then m.at(key) (associative containers). The order is observable in one place: a non-const std::map is reached through its operator[], so reading a missing key inserts it — exactly std::map's own semantics — while a const std::map reads through .at() and throws on a missing key.
Reads return references, never copies, and writes go through:
std::vector<double> values{1.0, 2.0};
auto view = mippp::views::mapping_all(values);
view[0u] = 3.0; // writes into `values`
double & v = view[1u]; // a reference, not a copy
Constness follows the std::ranges precedent: a mapping_ref_view is shallow-const (constness is carried by the referenced type — adapt a const lvalue to get const access), an owning view is deep-const.
Owning views stay std::movable even when they own a capturing lambda: assignment reconstructs the closure in place, and is only enabled when the move cannot throw. views::map(f) is the explicit spelling for lifting a callable; it copies an lvalue callable and moves an rvalue one:
auto squares = mippp::views::map([](int i) { return i * i; });
static_assert(mippp::input_mapping_of<decltype(squares), int, int>);
true_map, false_map, identity_map and element_map<I...> (chained std::get) are ready-made mapping views for the common projections.
Entity-keyed mappings¶
Model entities (variables, constraints) are strong types, so raw storage doesn't understand them. entity_mapping<Entity, Map> is the thin projection layer on top of mapping_all: a lookup passes the entity itself when the storage understands it — callables taking the entity, associative maps keyed by it — and otherwise falls back to the entity's uid():
using var = mippp::model_variable<int, double>;
mippp::entity_mapping<var, std::vector<double>> values =
std::vector<double>{1.0, 2.0};
values[var(0)] = 3.0; // uid() indexes the vector; writes go through
This is exactly what every solver's get_solution() / get_dual_solution() / get_reduced_costs() returns (as its variable_mapping / constraint_mapping aliases), which is why sol[X(i, j)] works uniformly whether the backend produced a flat array or a remapping closure.
Diagnostics¶
Storage that supports none of the three protocols is rejected with a message, not a template backtrace:
MIP++: this type cannot be used as a mapping with this key; it must provide operator, operator()(key) or at(key).
evaluate(expr, values_map) checks its values map up front the same way:
MIP++: evaluate needs a values map readable by the expression's variables; adapt raw storage or a callable with views::mapping_all or an entity_mapping.
The remedy it names is real: a bare lambda is not a mapping (no operator[]), so pass views::mapping_all(lambda) — or an entity_mapping — instead of the lambda itself.