Files
mp-units/docs/tutorials/incremental_migration.md
2026-01-21 23:37:24 +01:00

346 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tutorial 14: Incremental Migration from Legacy Units
Many codebases use home-grown strong-type wrappers or established units libraries like
Boost.Units or nholthaus/units. Rewriting everything at once is risky—massive code churn,
difficult validation, and team disruption. This tutorial demonstrates how to **safely extend
and incrementally migrate** using **mp-units' interoperability layer**, allowing old and
new code to coexist during transition.
You'll learn the **bridge pattern**: specialize `quantity_like_traits` to enable seamless
conversion between legacy types and **mp-units**, then **add new features** using mp-units
while keeping existing code working unchanged. For complete details on `quantity_like_traits`,
see the [Interoperability with Other Libraries](../how_to_guides/interoperability_with_other_libraries.md)
guide.
**Key migration strategy**: This tutorial demonstrates an asymmetric approach to interoperability.
Legacy types can flow naturally into new **mp-units** code, gaining safety and modern features.
However, when passing **mp-units** quantities back to legacy code, explicit conversions are
required. This intentional asymmetry provides visible integration points, makes the migration
measurable, and ensures the transition is gradual and safe.
## Problem Statement
Consider a physics simulation codebase that has evolved over 15 years using simple strong-type
wrappers for safety:
```cpp
// Home-grown strong types used throughout the codebase
struct Meter {
double value;
};
struct Second {
double value;
};
struct MeterPerSecond {
double value;
};
// Thousands of functions use these types
Meter legacy_calculate_distance(MeterPerSecond velocity, Second time)
{
return Meter{velocity.value * time.value};
}
bool legacy_is_safe_velocity(MeterPerSecond v)
{
return v.value < 100.0; // Safety limit: 100 m/s
}
// Typical usage throughout the codebase
void simulate_motion()
{
MeterPerSecond speed{30.0};
Second duration{5.0};
Meter distance = legacy_calculate_distance(speed, duration);
// Converting units requires manual factors
double distance_km = distance.value / 1000.0; // Manual conversion!
// Velocity validation
if (!legacy_is_safe_velocity(speed)) {
// handle unsafe velocity
}
}
```
**Problems with this approach:**
1. **No dimensional analysis**: `Meter{100} * Second{50}` compiles but produces nonsense
2. **Manual arithmetic**: Every calculation requires `.value` extraction and re-wrapping
3. **No unit conversions**: Converting km → m or km/h → m/s requires manual factors
4. **Limited scalability**: Fixed types for derived units (`MeterPerSecond`) don't
scale—**mp-units** uses unit equations (`m/s`, `km/h`) that compose automatically
5. **Verbose**: Simple physics becomes cluttered with `.value` everywhere
Your team wants to **extend** the library with new physics features (_mass_, _energy_)
using **mp-units**:
- ✅ Automatic dimensional analysis
- ✅ Natural arithmetic (`distance = velocity * time` just works)
- ✅ Built-in unit conversions (`.in(km)`, `.in(mi)`, `.in(kJ)`)
- ✅ Derived units automatically handled (_kinetic energy_ from `mass × speed²`)
- ✅ Modern C++20 features (concepts, formatting)
**But** you need the new features to work seamlessly with existing code:
- Existing motion calculations continue working unchanged
- New _energy_/_mass_ features use **mp-units**
- Legacy _velocity_ types can be passed to new functions
- Seamless interoperability at boundaries
## Your Task
Your goal is to **enable interoperability** between the legacy `Meter`, `Second`, and
`MeterPerSecond` types and **mp-units**, allowing the provided `compute_kinetic_energy()`
functions to work with legacy velocity types.
**Step 1**: Implement `quantity_like_traits` specializations for the three legacy types.
- Map `Meter``si::metre`, `Second``si::second`, `MeterPerSecond``si::metre / si::second`
- Set `explicit_import = false` (allow implicit legacy → **mp-units** conversion)
- Set `explicit_export = true` (require explicit **mp-units** → legacy conversion)
**Step 2**: Complete the provided functions to demonstrate new → old integration:
- In both `compute_kinetic_energy()` functions, convert `speed` to `MeterPerSecond` for
calling `legacy_is_safe_velocity()`
- This demonstrates explicit export: `MeterPerSecond(speed)` or
`MeterPerSecond(speed.in(m / s))`
- Observe: New **mp-units** code calls legacy validation seamlessly!
**Step 3**: Call the functions to see interoperability in action:
- Call both `compute_kinetic_energy()` functions with `70 * kg` and `legacy_velocity`
- Observe the difference: concrete type allows implicit conversion, generic requires explicit
**Requirements**:
- Implement `quantity_like_traits` for `Meter`, `Second`, and `MeterPerSecond`
(use `explicit_import = false` and `explicit_export = true`)
- Complete the energy functions by converting `speed` to `MeterPerSecond` for legacy validation
- Complete the `main()` function by calling the _energy_ calculation functions
- Don't modify the legacy struct definitions
```cpp
// ce-embed height=800 compiler=clang2110 flags="-std=c++23 -stdlib=libc++ -O3" mp-units=trunk
#include <mp-units/systems/isq.h>
#include <mp-units/systems/si.h>
#include <mp-units/math.h>
#include <iostream>
using namespace mp_units;
// Legacy strong types (DO NOT MODIFY)
struct Meter {
double value;
};
struct Second {
double value;
};
struct MeterPerSecond {
double value;
};
// Legacy functions (DO NOT MODIFY)
Meter legacy_calculate_distance(MeterPerSecond velocity, Second time)
{
return Meter{velocity.value * time.value};
}
bool legacy_is_safe_velocity(MeterPerSecond v)
{
return v.value < 100.0; // Safety limit: 100 m/s
}
// TODO: Step 1 - Specialize quantity_like_traits for Meter, Second, MeterPerSecond
// Hint:
// - Set reference to si::metre, si::second, si::metre / si::second
// - Set explicit_import = false (allow implicit legacy → mp-units)
// - Set explicit_export = true (require explicit mp-units → legacy)
// Modern functions (PARTIALLY PROVIDED - complete the legacy integration)
quantity<si::joule> compute_kinetic_energy(quantity<si::kilo<si::gram>> mass,
quantity<si::metre / si::second> speed)
{
using namespace mp_units::si::unit_symbols;
// TODO: Step 2a - Convert speed to MeterPerSecond to call legacy function
if (!legacy_is_safe_velocity(...))
throw std::runtime_error("Velocity exceeds safety limit!");
return 0.5 * mass * pow<2>(speed);
}
QuantityOf<isq::kinetic_energy> auto compute_kinetic_energy_generic(QuantityOf<isq::mass> auto mass,
QuantityOf<isq::speed> auto speed)
{
using namespace mp_units::si::unit_symbols;
// TODO: Step 2b - Convert speed to MeterPerSecond to call legacy function
if (!legacy_is_safe_velocity(...))
throw std::runtime_error("Velocity exceeds safety limit!");
return 0.5 * mass * pow<2>(speed);
}
int main()
{
MeterPerSecond legacy_velocity{25.0}; // 25 m/s
using namespace mp_units::si::unit_symbols;
// TODO: Step 3 - Call both energy functions with 70*kg and legacy_velocity
quantity energy = ...;
quantity energy_generic = ...;
std::cout << "KE = " << energy << " = " << energy.in(kJ) << "\n";
std::cout << "KE = " << energy_generic << " = " << energy_generic.in(kJ) << "\n";
}
```
??? "Solution"
```cpp
#include <mp-units/systems/isq.h>
#include <mp-units/systems/si.h>
#include <mp-units/math.h>
#include <iostream>
using namespace mp_units;
// Legacy strong types (DO NOT MODIFY)
struct Meter {
double value;
};
struct Second {
double value;
};
struct MeterPerSecond {
double value;
};
// Legacy functions (DO NOT MODIFY)
Meter legacy_calculate_distance(MeterPerSecond velocity, Second time)
{
return Meter{velocity.value * time.value};
}
bool legacy_is_safe_velocity(MeterPerSecond v)
{
return v.value < 100.0; // Safety limit: 100 m/s
}
// Specialize quantity_like_traits for legacy types
template<typename T>
constexpr auto reference_for = std::ignore;
template<> constexpr auto reference_for<Meter> = si::metre;
template<> constexpr auto reference_for<Second> = si::second;
template<> constexpr auto reference_for<MeterPerSecond> = si::metre / si::second;
template<typename T>
requires (!std::same_as<decltype(reference_for<T>), decltype(std::ignore)>)
struct mp_units::quantity_like_traits<T> {
using rep = double;
static constexpr auto reference = reference_for<T>;
// Allow implicit T → quantity (safe direction: legacy → modern)
static constexpr bool explicit_import = false;
// Require explicit quantity → T (make legacy conversions obvious)
static constexpr bool explicit_export = true;
static constexpr rep to_numerical_value(T m) { return m.value; }
static constexpr T from_numerical_value(rep v) { return T{v}; }
};
// Modern functions
quantity<si::joule> compute_kinetic_energy(quantity<si::kilo<si::gram>> mass,
quantity<si::metre / si::second> speed)
{
using namespace mp_units::si::unit_symbols;
// Demonstrate new → old integration: call legacy validation
// Need explicit export since explicit_export = true
if (!legacy_is_safe_velocity(MeterPerSecond(speed)))
throw std::runtime_error("Velocity exceeds safety limit!");
return 0.5 * mass * pow<2>(speed);
}
QuantityOf<isq::kinetic_energy> auto compute_kinetic_energy_generic(QuantityOf<isq::mass> auto mass,
QuantityOf<isq::speed> auto speed)
{
using namespace mp_units::si::unit_symbols;
// Same validation, same explicit export needed
if (!legacy_is_safe_velocity(MeterPerSecond(speed)))
throw std::runtime_error("Velocity exceeds safety limit!");
return 0.5 * mass * pow<2>(speed);
}
int main()
{
MeterPerSecond legacy_velocity{25.0}; // 25 m/s
using namespace mp_units::si::unit_symbols;
quantity energy = compute_kinetic_energy(70. * kg, legacy_velocity);
quantity energy_generic = compute_kinetic_energy_generic(70. * kg, quantity{legacy_velocity});
std::cout << "KE = " << energy << " = " << energy.in(kJ) << "\n";
std::cout << "KE = " << energy_generic << " = " << energy_generic.in(kJ) << "\n";
}
```
## References
- [Interoperability with Other Libraries](../how_to_guides/interoperability_with_other_libraries.md) -
Complete guide to `quantity_like_traits`
- [Working with Legacy Interfaces](../how_to_guides/working_with_legacy_interfaces.md) -
Extracting numerical values and working with `double`-based APIs
## Takeaways
**Key concepts learned:**
1. **`quantity_like_traits` enables interoperability** with legacy types without modifying them
2. **`QuantityOf` concept vs concrete types** - critical API design choice:
- `quantity_like` types don't satisfy `QuantityOf` concept (missing static data
members (e.g., `reference`, `unit`, ...) and many member functions like `.in()`)
- Generic functions with `QuantityOf` constraints: More flexible (accept any
compatible units), but require explicit `quantity{...}` construction for
`quantity_like` types
- Concrete functions with `quantity<...>` parameters: Less flexible (fixed units),
but allow implicit conversion from `quantity_like`
- Choose based on: API flexibility needs vs call-site convenience
3. **Bidirectional integration patterns**:
- Legacy → New (implicit): `explicit_import = false` allows legacy types to flow naturally
into **mp-units** code, gaining dimensional safety
(e.g., `compute_kinetic_energy(70*kg, legacy_velocity)`)
- New → Legacy (explicit): `explicit_export = true` requires visible syntax when calling
legacy functions from **mp-units** (e.g., `legacy_is_safe_velocity(MeterPerSecond(speed))`)
- Unit conversions happen automatically in both directions (e.g., `km/h` ↔ `m/s`)
**Why explicit export helps migration:**
| Aspect | `explicit_export = true` | `explicit_export = false` |
|-----------------------|-------------------------------------------------------------|--------------------------------------------------------------------|
| **Syntax** | `LegacyType(quantity)` visible | Hidden in function calls |
| **Searchability** | Grep for `MeterPerSecond(` finds all | Hard to find integration points |
| **Refactoring** | Compiler guards each boundary crossing | Silent conversions might hide issues |
| **Progress tracking** | Count explicit casts = legacy debt | No clear metric |
| **Team clarity** | "This crosses into legacy land" | Boundary invisible to reviewers |
| **When to use** | **Recommended for simple legacy wrappers** during migration | Established libraries (Boost.Units) with similar safety guarantees |
**Real-world benefits:**
-**Zero disruption**: Legacy code continues working unchanged
-**Immediate value**: New code gains dimensional analysis, unit conversions, modern C++20
-**Gradual learning**: Team learns **mp-units** while delivering features
-**Clear metrics**: Track migration progress (% of modules migrated)
-**Risk reduction**: Validate each module before moving to the next