One model, any solver — at raw C API speed.
MIP++ is a header-only, dependency-free C++23 library for linear, mixed-integer, and quadratic programming. It gives you an algebraic modeling syntax as readable as JuMP or Pyomo, but compiles down to direct calls into the solver's own C API — no intermediate model representation, no extraction step, no allocation in the expression layer. The same model code targets any of 11 solvers; you choose the backend at compile time, and its shared library is discovered and loaded dynamically at runtime, with no link-time solver dependency.
📖 Documentation — start with the Getting Started guide.
#include <print>
#include "mippp/solvers/highs/all.hpp"
using namespace mippp;
using namespace mippp::operators;
int main() {
highs_lp model; // loads HiGHS at runtime, swap for gurobi_*, cbc_*, ...
auto x1 = model.add_variable();
auto x2 = model.add_variable({.upper_bound = 3});
model.set_maximization();
model.set_objective(4 * x1 + 5 * x2);
model.add_constraint(x1 <= 4);
model.add_constraint(2 * x1 + x2 <= 9);
model.solve();
auto sol = model.get_solution();
std::println("objective = {}", model.get_solution_value());
std::println("x1 = {}, x2 = {}", sol[x1], sol[x2]);
return 0;
}The Getting Started guide walks through this model, the expression system, and solver selection. Runnable examples — N-Queens, sudoku, TSP with lazy subtour elimination, cutting stock via column generation, an infeasible transportation plan diagnosed through its IIS — live in examples/.
Benchmarking Gurobi vs. CPLEX vs. HiGHS vs. SCIP is a two-line change and a recompile — no #ifdef soup, no linking against a solver SDK. Solver shared libraries are discovered at runtime via exact path parameter, per-solver environment variables (MIPPP_HIGHS_LIBRARY, etc.) or LD_LIBRARY_PATH and system directories: a single compiled binary runs on whatever solver the machine has installed. (Why MIP++)
Filling a model through MIP++ costs 102–108% of what calling the solver's own C API costs. Against the other modeling layers, on the same backend: 1.2–5.6× faster than OR-Tools' two C++ APIs, 3.7–18× faster than JuMP, and one to two orders of magnitude faster than the Python interfaces — all measured on model construction alone, never on solving. Build time is noise for a one-shot solve that runs for hours, but can dominate in algorithms that build, modify, and re-solve constantly: column generation, Benders decomposition, cutting planes, large-scale experiments. MIP++ is built for those. See the benchmark ↓
The speedup comes from the expression system's architecture: objectives and constraints are composed from C++ ranges as lazy views using xsum, and they allocate nothing. When a constraint is added, its term range is iterated directly into the pre-allocated scratch buffers passed to the solver's C API (Highs_addRow, GRBaddconstr, etc.). There is no intermediate model representation to build, extract, or garbage-collect. (Expressions and constraints)
A MIP++ model is the solver's native model. There is no extraction step, so re-solves after modifications (adding variables, changing bounds, adding constraints) pay only the solver's incremental update cost, never a full model rebuild.
The library exposes the algorithmic hooks that decomposition and cutting-plane methods need:
- Branch-and-cut callbacks with lazy constraints — the TSP example in
examples/travelling_salesman_dfj/adds subtour elimination cuts through a typed callback handle using the samexsumsyntax as the main model. - Column generation via
add_column, dual values (get_dual_solution), and reduced costs (get_reduced_costs). A fullcolumn_managerframework tracks columns across pool and master states, propagates pricing events to per-column properties (reduced cost, primal value, age) through compile-time event dispatch, and provides pluggable activation/eviction strategies — all at zero runtime overhead for unused properties. - MIP starts, indicator constraints, and in-place model updates (bound changes, coefficient changes, variable removal). When columns are evicted, solvers compact their internal arrays, invalidating external indices. MIP++ keeps user-facing variable handles stable through a bidirectional handle/native-ID map — but the map is only allocated on the first deletion; models that never remove variables pay nothing beyond a branch prediction.
Solver-agnostic code usually hides backend-specific outcomes behind a lowest-common-denominator enum. MIP++ does the opposite: each backend's get_status() returns a std::variant whose alternatives are exactly the statuses that solver actually reports. MOSEK's LP variant distinguishes primal_and_dual_infeasible from plain infeasible; Clp's carries nothing beyond unknown (the pre-solve state every backend starts in), optimal, infeasible, and unbounded. Nothing is erased.
Generic queries work through the status type hierarchy: primal_and_dual_infeasible inherits from infeasible, which inherits from infeasible_or_unbounded. Calling is_a<status::infeasible_or_unbounded> matches any of them — one question, every solver. Calling is<status::primal_and_dual_infeasible> asks the exact question instead, and will only compile if the backend can report it. This abstraction is free at runtime — the inheritance check is resolved at compile time, so the compiler reduces it to a direct index check. (Status and limits)
Time to build the N-Queens model (N² binary variables, 6N−6 constraints) through eight modeling libraries across C++, Julia and Python. Only model construction is timed, never the resolution: the timer stops once the model holds every variable and constraint, after flushing any pending update.
The most direct measure of modeling overhead — the raw C API in milliseconds, MIP++ as a percentage of it, the other Gurobi-capable interfaces as multiples:
| N | Gurobi C API | MIP++ | gurobipy | JuMP direct | Python-MIP |
|---|---|---|---|---|---|
| 100 | 3.2 ms | 102% | 6.5× | 3.5× | 19.4× |
| 500 | 48.2 ms | 104% | 9.6× | 7.5× | 17.8× |
| 1000 | 190.0 ms | 107% | 12.3× | 7.4× | 18.5× |
MIP++ stays within 2–8% of the raw C API across the whole sweep: the abstraction layer is thin. Handing Gurobi the entire matrix in one GRBaddconstrs call rather than one GRBaddconstr per constraint is worth nothing, so matching the per-constraint path is the meaningful comparison rather than a handicap.
MIP++ in absolute milliseconds, the other interfaces as multiples of it on the same backend (HiGHS, the one backend all of them support):
| N | MIP++ | OR-Tools MPSolver |
OR-Tools MathOpt | JuMP cached | JuMP direct |
|---|---|---|---|---|---|
| 100 | 1.6 ms | 1.3× | 2.6× | 3.7× | 12.0× |
| 500 | 37.4 ms | 1.3× | 3.7× | 6.1× | 16.1× |
| 1000 | 151.5 ms | 1.3× | 5.6× | 5.7× | 18.3× |
Both OR-Tools APIs are benchmarked in their fastest row-filling form — coefficients written straight into an opened row, worth ~1.8× for MPSolver and ~1.2× for MathOpt over the idiomatic expression object — so OR-Tools is shown at its best. On Cbc the gap against MPSolver is wider than on HiGHS: 2.4–3.0× across the sweep, MIP++ filling the model in 66.7 ms at N=1000.
Note
The comparison is not work-for-work, and the bias is against MIP++. OR-Tools' two APIs and JuMP's default Model accumulate the model in their own structures and hand it to the solver later; MIP++, the Gurobi C API and JuMP's direct_model write into the solver's own model as each constraint is added. Two tells: JuMP cached fills at the same speed whichever backend is named (867 ms HiGHS, 873 ms Gurobi at N=1000), and MPSolver needs ~200–220 ms at N=1000 for Cbc, HiGHS and SCIP alike. This is also why OR-Tools appears faster on SCIP (45% of MIP++) — it has not handed SCIP anything yet.
Against the Python interfaces on the same backend, MIP++ is 11× faster than gurobipy, 54× than Python-MIP, 103× than PuLP and 128× than highspy.
Warning
The Cbc figures were measured against Cbc's devel branch, the only version that caches addRow; release 2.10.13 (what coinor-libcbc-dev ships) flushes the matrix on every call and is substantially slower for every interface that builds directly in Cbc.
N-Queens is a variable-heavy, constraint-light model, measured on a single machine and compiler — the ratios transfer, the absolute times do not.
Full tables (N=100–1000 in steps of 100), per-backend results for eight solvers, build-variant comparisons, limitations, and reproduction instructions: Performance — benchmark code in mippp_nqueens.
| Backend | LP | MILP | QP |
|---|---|---|---|
| HiGHS | ✓ | ✓ | ✓ |
| Gurobi, CPLEX, Xpress, COPT, MOSEK, GLPK | ✓ | ✓ | |
| SCIP | ✓ | ||
| Clp, SoPlex | ✓ | ||
| Cbc (experimental) | ✓ |
Per-feature support (duals, reduced costs, callbacks, MIP starts, indicator constraints, variable removal, IIS, …) varies by backend — see the feature matrices in Choosing a solver.
MIP++ has a deliberate niche. It is worth being honest about where it fits and where it doesn't.
Optimization embedded in a larger C++ system. A simulator, a planning service, a shipped binary that must run with whatever solver the user has installed. The runtime dlopen-based solver loading means no recompilation per solver and no link-time SDK dependency — this is the sweet spot.
Cross-solver experiments. Reviewers asking for results on Gurobi and an open solver, licenses that differ between your laptop, the cluster, and your coauthors' machines. The two-line backend switch was built for exactly this.
Build-bound iterative methods. Column generation, cutting planes, Benders decomposition, iterated reoptimization. In-place model updates, add_column with a full column pool manager, and lazy-constraint callbacks are here today.
Everyday modeling in Python or Julia. Stay with gurobipy, JuMP, or Pyomo. They are mature, their communities are large, and for a one-shot solve where solver time dominates, the modeling overhead rarely matters.
Heavy solver-specific parameter tuning. There is no uniform interface for solver-specific knobs. They stay reachable — model.native_model() hands out the solver's own handles and the *_api objects expose the raw C entry points — but each such call is backend-specific code.
Constraint programming or scheduling. Use OR-Tools CP-SAT or a dedicated CP solver.
Important
MIP++ requires GCC 14+ / C++23 (GCC 15 / C++26 recommended) and assumes comfort with modern C++ — ranges, concepts, and template diagnostics. It is a young, single-maintainer project; contributions are welcome, but pin a version if you build long-lived research code on it.
MIP++ is header-only and has no library dependency — solver libraries are opened at runtime through the platform loader (dlopen / LoadLibrary), which MIP++ wraps itself. Install via Conan, as a CMake subdirectory, or with a plain CMake install:
git clone https://github.com/fhamonic/mippp && cd mippp
conan create . -u -b=missing -pr=<your_conan_profile> # Conan
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=<prefix> && cmake --install build # CMake packageSolver shared libraries are discovered at runtime; only the solvers you actually have installed need to be present, and nothing solver-related is needed at compile time. Per-solver environment variables (MIPPP_HIGHS_LIBRARY, MIPPP_GUROBI_LIBRARY, etc.) can pin specific library paths or versioned sonames. Full instructions: Installation.
The modeling core is in place: LP/MILP/QP, lazy-constraint callbacks, column generation with a pool manager, reduced costs, MIP starts, indicator constraints, in-place model updates, infeasibility diagnosis by irreducible infeasible subsystem (IIS), through the solver's own routine on Gurobi, CPLEX, Xpress, COPT and HiGHS and a deletion filter on every backend.
Planned, roughly by priority:
| Priority | Feature | Notes |
|---|---|---|
| 🔴 | LP basis warm-starts | Concepts specified (has_lp_basis, has_lp_basis_warm_start); no backend implements get_basis/set_basis yet |
| 🔴 | User-cut callbacks | For cutting-plane methods at node relaxations |
| 🔴 | Heuristic-solution injection | Injecting primal solutions from within callbacks |
| 🟠 | SOS1/SOS2 constraints | Concepts specified (has_sos1_constraints, has_sos2_constraints); no backend implements add_sos1_constraint/add_sos2_constraint yet |
| 🟠 | QP objectives beyond HiGHS | Extend Hessian support to Gurobi, CPLEX, MOSEK, etc. |
| 🟡 | QCP/SOCP constraints | Quadratically constrained programs |
| 🟡 | Model file I/O | Read/write LP and MPS files |
| 🟡 | MILP fuzzy tests | Differential fuzzing of the MILP interface against a reference implementation, as lp_fuzzy_tests does for LPs; the hand-written suites left gaps in the backends |
| ⚪ | Solution pools, multi-objective, semi-continuous variables, progress getters |
Note
Since a MIP++ model is the solver's native model, re-solves after in-place modifications (adding rows, changing bounds) almost always warm-start from the last basis implicitly. The roadmap item above is about explicit basis get/set — transferring a basis between models or storing one for later.
The first three items are what build-bound, re-solve-heavy research code wants most. Until they land, a column-generation or cutting-plane study hitting those specific features may still be better served by a direct solver API — and the honest comparison is in the Is MIP++ for you? section above.
Contributions are welcome — see CONTRIBUTING.md, and open an issue to claim an item.
MIP++ is grounded in the PhD thesis and postdoctoral positions of François Hamonic, funded by Région Sud - Provence-Alpes-Côte d'Azur, Natural Solutions, the European Research Council grant SCALED to Cécile ALBERT (ERC-STG no. 949812), the ANR project RESILIENCE (no. ANR-24-PEVD-0002) and the project OASIS of ITEM, an A*Midex Initiative d'Excellence institute funded under France 2030 (AMX-19-IET-012).
- 📖 Documentation: fhamonic.github.io/mippp — sources live under
docs/. - 📄 If you use MIP++ in academic work, please cite it — see
CITATION.cff. - ⚖️ Licensed under the Boost Software License 1.0.
