Extending Beyond Physics: Currency as Custom Dimension¶
Try it live on Compiler Explorer
Overview¶
mp-units is a general-purpose quantities and units library — everything that can be counted or measured benefits from its type-safe dimensional analysis. This example demonstrates creating a custom base dimension for currency and implementing type-safe currency conversions with exchange rates.
Key Concepts¶
Custom Base Dimension¶
You can define entirely new base dimensions beyond the standard ISQ/SI system:
This creates a new fundamental dimension orthogonal to physical dimensions like length or mass.
Custom Units and Quantity Specifications¶
Once you have a base dimension, define quantity specification and its units:
QUANTITY_SPEC(currency, dim_currency);
inline constexpr struct euro final : named_unit<"EUR", kind_of<currency>> {} euro;
inline constexpr struct us_dollar final : named_unit<"USD", kind_of<currency>> {} us_dollar;
inline constexpr struct great_british_pound final : named_unit<"GBP", kind_of<currency>> {} great_british_pound;
inline constexpr struct japanese_jen final : named_unit<"JPY", kind_of<currency>> {} japanese_jen;
// clang-format on
namespace unit_symbols {
inline constexpr auto EUR = euro;
inline constexpr auto USD = us_dollar;
inline constexpr auto GBP = great_british_pound;
inline constexpr auto JPY = japanese_jen;
} // namespace unit_symbols
Each currency becomes its own distinct unit, preventing accidental mixing of incompatible currencies:
This won't compile — you cannot directly compare EUR and USD quantities, as they are defined as independent units of currency with no compile-time known conversion factor between them. The library framework cannot perform the conversion automatically because exchange rates are time-dependent, and it doesn't know at what time point (now, yesterday, etc.) the user wants to make the conversion.
User-Provided Runtime Conversion Functions¶
Since currency exchange rates are time-dependent, the library framework cannot automatically convert between currencies of independent units. Instead, users must provide their own runtime conversion functions that account for specific time points.
The example implements a time-dependent conversion function:
template<Unit auto From, Unit auto To>
[[nodiscard]] double exchange_rate(std::chrono::sys_seconds timestamp)
{
(void)timestamp; // get conversion ratios for this timestamp
static const std::map<std::pair<std::string_view, std::string_view>, double> rates = {
{{"USD", "EUR"}, 0.9215}, {{"EUR", "USD"}, 1.0848},
// ...
};
return rates.at(std::make_pair(unit_symbol(From), unit_symbol(To)));
}
The exchange_to function takes a timestamp parameter and applies the appropriate conversion
factor for that specific point in time:
template<UnitOf<currency> auto To, QuantityOf<currency> From>
QuantityOf<currency> auto exchange_to(From q, std::chrono::sys_seconds timestamp)
{
const auto rate = static_cast<From::rep>(exchange_rate<From::unit, To>(timestamp) * q.numerical_value_in(q.unit));
return rate * From::quantity_spec[To];
}
This pattern allows users to handle conversions between units of the same quantity type when the conversion rate cannot be known at compile time and varies based on external factors like time.
Type Safety with Quantity Points¶
The example also demonstrates currency exchange using quantity points, which can be useful for representing prices with a reference point:
template<UnitOf<currency> auto To, QuantityPointOf<currency> From>
QuantityPointOf<currency> auto exchange_to(From qp, std::chrono::sys_seconds timestamp)
{
const auto rate = static_cast<From::rep>(exchange_rate<From::unit, To>(timestamp) *
qp.quantity_from_zero().numerical_value_in(qp.unit));
return quantity_point{rate * From::quantity_spec[To], From::point_origin};
}
Example Usage¶
using namespace unit_symbols;
using namespace std::chrono;
const auto timestamp = time_point_cast<seconds>(system_clock::now() - hours{24});
const quantity_point price_usd{100 * USD};
const quantity_point price_euro = exchange_to<euro>(price_usd, timestamp);
std::cout << price_usd.quantity_from_zero() << " -> " << price_euro.quantity_from_zero() << "\n";
// std::cout << price_usd.quantity_from_zero() + price_euro.quantity_from_zero() << "\n"; // does not compile
Sample Output:
Why This Matters¶
- General-Purpose Library: Demonstrates mp-units working with non-physical measurable quantities
- Type Safety for Finance: Prevents mixing currencies without explicit conversion
- Domain Modeling: The dimensional analysis pattern applies to any domain with unit-like concepts
- Time-Dependent Conversions: Shows how to handle conversions where the rate depends on external factors (time, location, market conditions) by providing user-defined runtime functions
This pattern can be adapted for other non-physical domains: cryptocurrencies, points/rewards systems, game economies, or any system where different "units" should not be accidentally mixed.
Practical Considerations¶
Currency conversions differ from typical physical unit conversions:
- Exchange rates are time-dependent — the framework cannot know when you want to convert
- Users must provide runtime conversion functions that account for specific time points
- Real-world factors like bid/ask spreads and transaction costs should be incorporated in the user-provided conversion function
- Different markets may have different rates at the same time
- Unlike physical constants (meters to kilometers), there's no fixed mathematical relationship between currencies
- This example provides a simplified model for demonstration purposes