Tutorial 11: Preventing Confusion with Distinct Kinds¶
Many engineering domains have quantities that share the same physical dimension but represent fundamentally different concepts. Hydraulic engineering uses "head"—a measure of potential energy per unit weight expressed as an equivalent height—in two incompatible ways:
- Fluid head: Potential energy normalized to the actual fluid's density (e.g., 2 m of mercury)
- Water head: Potential energy normalized to water's density (e.g., 27.2 m water equivalent)
Both express energy using length dimensions, but mixing them produces physically
meaningless results—like mixing gauge and absolute pressure without conversion.
Traditional code using raw double values allows such mistakes to silently compile.
Remarkably, even units libraries from C++ and also other programming languages cannot
prevent this error—they only check dimensional compatibility, not physical meaning.
This tutorial demonstrates how mp-units uses is_kind to create distinct quantity
subkinds within an existing hierarchy—a capability unique among units libraries worldwide.
The key insight: is_kind lets quantities inherit properties (dimension, unit) from a
parent while isolating them from each other. Just as the library prevents mixing
plane angles and solid angles (both subkinds of dimensionless), you can create
custom subkinds like fluid head and water head (both subkinds of height) that
cannot be accidentally mixed.
Problem Statement¶
Consider a pump system design for a chemical processing plant. Engineers must verify that the pump capacity is adequate for the fluid being handled. This requires comparing:
- System requirement: The fluid column that must be lifted, expressed as fluid head (energy normalized to the actual fluid's density)
- Pump specifications: Rated in water head (energy normalized to water density)
The relationship between fluid head and water head reflects energy conservation with different density normalizations:
Where specific gravity is the dimensionless ratio of fluid density to water density. The same potential energy is expressed as a larger height for lighter fluids (water) and a smaller height for denser fluids (mercury).
Here's how different approaches handle (or fail to handle) this scenario:
// Traditional approach - all heights are just doubles
double h_mercury_m = 2.0; // Height of mercury column
double h_pump_rating_m = 10.0; // Pump rated for water
double sg_mercury = 13.6; // Specific gravity
// Direct addition - compiles but physically wrong!
double total_head = h_mercury_m + h_pump_rating_m; // 12 m - WRONG!
// This treats 2 m of mercury as if it were 2 m of water
// Correct calculation requires manual tracking:
double h_mercury_as_water = h_mercury_m * sg_mercury; // 27.2 m
// Compare system requirement vs pump capacity
if (h_mercury_as_water > h_pump_rating_m) {
std::cout << "Pump is undersized!\n"; // This will trigger!
}
#include <boost/units/systems/si.hpp>
using namespace boost::units;
using namespace boost::units::si;
quantity<length> h_mercury = 2.0 * meters;
quantity<length> h_pump_rating = 10.0 * meters;
double sg_mercury = 13.6;
// Direct addition - compiles but physically wrong!
quantity<length> total_head = h_mercury + h_pump_rating; // WRONG!
// Both are lengths, so Boost.Units allows this
// Correct calculation still requires manual tracking:
quantity<length> h_mercury_as_water = h_mercury * sg_mercury;
// Compare system requirement vs pump capacity
if (h_mercury_as_water > h_pump_rating) {
std::cout << "Pump is undersized!\n"; // This will trigger!
}
Problem: Boost.Units checks dimensional compatibility (both are lengths), but cannot distinguish between physically incompatible types of length.
import pint
ureg = pint.UnitRegistry()
h_mercury = 2.0 * ureg.meter
h_pump_rating = 10.0 * ureg.meter
sg_mercury = 13.6
# Direct addition - works but physically wrong!
total_head = h_mercury + h_pump_rating # WRONG!
# Both have dimension [length], so Pint allows this
# Correct calculation still requires manual tracking:
h_mercury_as_water = h_mercury * sg_mercury
# Compare system requirement vs pump capacity
if h_mercury_as_water > h_pump_rating:
print("Pump is undersized!") # This will trigger!
Problem: Pint prevents dimensional errors but cannot distinguish between different physical meanings of the same dimension.
The fundamental limitation: Units libraries check dimensional compatibility
(length + length = OK), but cannot enforce that quantities with the same dimension
may represent incompatible physical concepts. This is where mp-units breaks new
ground with its is_kind feature.
Problems common to all these approaches:
- No distinction: Both fluid head and water head have the same dimensional type (length), making them indistinguishable to the type system
- Silent errors: Adding incompatible head types compiles successfully but produces physically nonsense results
- Manual tracking: Programmers must remember which variables represent which type of head—the type system provides no help
- Comparison confusion:
2 m < 10 mnumerically, but2 mof mercury represents far more energy (and pressure) than10 mof water - Easy to forget: Forgetting the SG conversion factor leads to severely undersized equipment—a potentially catastrophic error in chemical plants
Real-world scenario:
A chemical plant pump system must:
- Handle mercury (SG = 13.6) from a 2 m column in a reactor vessel
- Verify a pump rated for 10 m water head can handle this load
- Convert the mercury fluid head to equivalent water head for comparison
- Prevent accidentally mixing fluid head and water head values
- Require explicit conversion through specific gravity
The challenge: Both are heights (dimension: length), but they're physically incompatible without conversion through specific gravity.
Your task¶
Implement a type-safe hydraulic head calculation system using mp-units that prevents mixing fluid head and water head without explicit conversion.
Create:
- Distinct kinds: Define
fluid_headandwater_headas separate kinds derived fromisq::height - Specific gravity type: Define
specific_gravityas a dimensionlessquantity_spec - Conversion functions: Implement type-safe conversions between the two head types:
to_water_head(h_fluid, sg)— converts fluid head to water head using SGto_fluid_head(h_water, sg)— converts water head to fluid head using SG
The solution should:
- Prevent direct addition or comparison of fluid head and water head (compile-time error)
- Require explicit conversion through specific gravity
- Use
QuantityOfconstraints for type safety - Work with any units of length (meters, feet, etc.)
// ce-embed height=800 compiler=clang2110 flags="-std=c++23 -stdlib=libc++ -O3" mp-units=trunk
#include <mp-units/systems/si.h>
#include <iostream>
using namespace mp_units;
// TODO: Define fluid_head as a distinct kind derived from isq::height
// TODO: Define water_head as a distinct kind derived from isq::height
// TODO: Define specific_gravity as a dimensionless quantity_spec
// TODO: Implement to_water_head conversion function
// Formula: H_water = H_fluid * SG
// Hint: Return type should be QuantityOf<water_head> auto
// TODO: Implement to_fluid_head conversion function
// Formula: H_fluid = H_water / SG
int main()
{
using namespace si::unit_symbols;
// Scenario: Chemical reactor with 2m mercury column (SG = 13.6)
quantity h_mercury = fluid_head(2 * m);
quantity sg_mercury = specific_gravity(13.6 * one);
// Pump rated for 10m water head
quantity h_pump_rating = water_head(10 * m);
std::cout << "Pump System Design Analysis\n";
std::cout << "============================\n\n";
std::cout << "Mercury column height: " << h_mercury << "\n";
std::cout << "Mercury specific gravity: " << sg_mercury << "\n";
std::cout << "Pump rating (water head): " << h_pump_rating << "\n\n";
// Safety check: This should NOT compile!
// quantity wrong = h_mercury + h_pump_rating; // Error: cannot mix kinds
// Convert mercury fluid head to equivalent water head
quantity h_mercury_as_water = to_water_head(h_mercury, sg_mercury);
std::cout << "Mercury equivalent (water head): " << h_mercury_as_water << "\n\n";
// Verify pump capacity against system requirement
if (h_mercury_as_water > h_pump_rating) {
std::cout << "WARNING: System requirement (" << h_mercury_as_water
<< ") exceeds pump rating (" << h_pump_rating << ")!\n";
std::cout << "Pump is UNDERSIZED for this application.\n";
}
else {
quantity excess_capacity = h_pump_rating - h_mercury_as_water;
std::cout << "Pump capacity is adequate.\n";
std::cout << "Excess capacity: " << excess_capacity << "\n";
}
// Demonstrate reverse conversion
quantity h_back_to_fluid = to_fluid_head(h_mercury_as_water, sg_mercury);
std::cout << "\nVerification - converted back: " << h_back_to_fluid << "\n";
}
Solution
#include <mp-units/systems/si.h>
#include <iostream>
using namespace mp_units;
// 1. Define the distinct kinds (The Safety Layer)
inline constexpr struct fluid_head final : quantity_spec<isq::height, is_kind> {} fluid_head;
inline constexpr struct water_head final : quantity_spec<isq::height, is_kind> {} water_head;
// 2. Define a type for Specific Gravity (Dimensionless)
inline constexpr struct specific_gravity final : quantity_spec<dimensionless> {} specific_gravity;
// 3. Define Conversion Helpers
// Formula: H_water = H_fluid * SG
constexpr QuantityOf<water_head> auto to_water_head(QuantityOf<fluid_head> auto h_fluid,
QuantityOf<specific_gravity> auto sg)
{
// We explicitly cast the result to water_head because we know the physics is correct
return water_head(isq::height(h_fluid) * sg);
}
// Formula: H_fluid = H_water / SG
constexpr QuantityOf<fluid_head> auto to_fluid_head(QuantityOf<water_head> auto h_water,
QuantityOf<specific_gravity> auto sg)
{
return fluid_head(isq::height(h_water) / sg);
}
int main()
{
using namespace si::unit_symbols;
// Scenario: Chemical reactor with 2m mercury column (SG = 13.6)
quantity h_mercury = fluid_head(2 * m);
quantity sg_mercury = specific_gravity(13.6 * one);
// Pump rated for 10m water head
quantity h_pump_rating = water_head(10 * m);
std::cout << "Pump System Design Analysis\n";
std::cout << "============================\n\n";
std::cout << "Mercury column height: " << h_mercury << "\n";
std::cout << "Mercury specific gravity: " << sg_mercury << "\n";
std::cout << "Pump rating (water head): " << h_pump_rating << "\n\n";
// Safety check: This would NOT compile!
// quantity wrong = h_mercury + h_pump_rating; // Error: cannot mix kinds
// Convert mercury fluid head to equivalent water head
quantity h_mercury_as_water = to_water_head(h_mercury, sg_mercury);
std::cout << "Mercury equivalent (water head): " << h_mercury_as_water << "\n\n";
// Verify pump capacity against system requirement
if (h_mercury_as_water > h_pump_rating) {
std::cout << "WARNING: System requirement (" << h_mercury_as_water
<< ") exceeds pump rating (" << h_pump_rating << ")!\n";
std::cout << "Pump is UNDERSIZED for this application.\n";
}
else {
quantity excess_capacity = h_pump_rating - h_mercury_as_water;
std::cout << "Pump capacity is adequate.\n";
std::cout << "Excess capacity: " << excess_capacity << "\n";
}
// Demonstrate reverse conversion
quantity h_back_to_fluid = to_fluid_head(h_mercury_as_water, sg_mercury);
std::cout << "\nVerification - converted back: " << h_back_to_fluid << "\n";
}
How the solution works:
By marking fluid_head and water_head with is_kind, we create distinct quantity types
that cannot be mixed despite sharing the length dimension:
-
Compile-time prevention: Direct addition, comparison, or assignment between fluid head and water head results in a compile error
-
Explicit conversion required: The
to_water_headandto_fluid_headfunctions perform the physics-based conversion through specific gravity, making the conversion visible and intentional in the code -
Type safety at boundaries: Functions accepting
QuantityOf<fluid_head>orQuantityOf<water_head>cannot accidentally receive the wrong type -
Base quantity access: When needed, both can be converted to
isq::heightusingisq::height(h), allowing generic height operations while preserving type safety at domain boundaries
This pattern is similar to how mp-units prevents mixing plane angles and solid angles— both dimensionless quantities that share the same dimension but represent fundamentally different physical concepts that cannot be meaningfully combined.
References¶
Takeaways¶
is_kindcreates incompatible types: Even when quantities share the same dimension,is_kindprevents mixing them without explicit conversion- Domain-specific safety: Hydraulic engineering's distinction between energy measurements in different reference frames (fluid head vs water head) becomes a compile-time guarantee
- Explicit conversions: Physics-based conversions (through specific gravity) are visible and required in the code
- Prevents subtle bugs: The classic mistake of treating 2 m of mercury as 2 m of water becomes a compile error
- Type system as documentation: The code itself documents that these are different physical concepts requiring conversion
- Similar to built-in protections: Just as mp-units prevents mixing radians and steradians (angular measure vs solid angular measure), your domain can have custom protections
- Explicit base conversion when needed: Both can convert to generic
isq::heightusingisq::height(h)for algorithms that work on any length—but this requires an explicit conversion call; implicit conversion will fail to compile, preserving type safety at domain boundaries - Real-world safety: Equipment undersizing due to head calculation errors can be catastrophic in chemical plants—type safety prevents this
- Pattern for other domains: This technique applies anywhere quantities share dimensions but represent incompatible concepts—particularly energy or power measurements in different reference frames (e.g., gauge vs absolute pressure, RMS vs peak voltage, true vs apparent power)