Skip to content

Tutorial 2: Refactor to Generic, Type-Safe Interfaces

In this exercise, you’ll refactor a function that computes a derived quantity from quantities measured in SI base units, where the arguments may use different prefixes or scaled units.

For this exercise, we’ll use a real-world example from engineering and meteorology: surface mass density (mass per area). This is used for things like snow load on roofs, coating thickness, or areal density of materials, and results in a derived unit (kg/m², g/cm², etc.) that does not have a special SI name.

The goal is to make the function generic and type-safe using C++ concepts, so it works for any compatible units and prevents scaling errors at compile time.

Problem statement

Suppose you have a function that computes surface mass density \(\sigma = \frac{m}{A}\) (mass divided by area), but the current version only accepts mass in kilograms and area in square meters, returning the result in kg/m².

quantity<si::kilogram / square(si::metre)> surface_mass_density(quantity<si::kilogram> mass,
                                                                quantity<square(si::metre)> area)
{
  return mass / area;
}

In real-world scenarios, you might have mass in grams and area in square centimeters, and you want the function to work generically and safely for any compatible units without unnecessary conversions that may affect your runtime performance or truncate data.

Your task

Refactor the function so it accepts any quantity representing mass and any quantity representing area, regardless of their specific units or prefixes. Use QuantityOf concepts to constrain the arguments, ensuring only quantities of mass and area are accepted.

The function should return a quantity representing surface mass density, with the unit determined by the division of the input units (e.g., g/cm², kg/m², etc.).

When you are done, try making a few intentional mistakes and observe the compile-time error messages. For example:

  • pass length instead of mass, or volume instead of area
  • change the equation in the function's body to multiply instead of divide the its arguments (typical programmer's mistake).

This will help you understand how concepts enforce type safety.

// ce-embed height=650 compiler=clang2110 flags="-std=c++23 -stdlib=libc++ -O3"
#include <mp-units/systems/isq.h>
#include <mp-units/systems/si.h>
#include <iostream>

using namespace mp_units;
using namespace mp_units::si::unit_symbols;

// TODO: Refactor to a generic, type-safe version
// - Use concepts to accept any mass and area quantities (with any units or prefixes)
// - The return type should be constrained to isq::surface_mass_density quantity
//   and accept any compatible units
// - No unit scaling should be applied; the result should reflect the direct division
constexpr quantity<si::kilogram / square(si::metre)> surface_mass_density(quantity<si::kilogram> mass,
                                                                          quantity<square(si::metre)> area)
{
  return mass / area;
}

int main()
{
  constexpr Unit auto cm2 = cm * cm;
  quantity mass = 350. * g;         // e.g., snow load on a small roof patch
  quantity area = 5000. * cm2;      // 0.5 square meters
  quantity res = surface_mass_density(mass, area);
  // Convert the result to a specific unit if needed (e.g., g/cm²)
  quantity res_in_g_per_cm2 = res.in(g / cm2);
  std::cout << "res = " << res << '\n';
  std::cout << "res in g/cm² = " << res_in_g_per_cm2 << '\n';
}
Solution
#include <mp-units/systems/isq.h>
#include <mp-units/systems/si.h>
#include <iostream>

using namespace mp_units;
using namespace mp_units::si::unit_symbols;

constexpr QuantityOf<isq::surface_mass_density> auto surface_mass_density(QuantityOf<isq::mass> auto mass,
                                                                          QuantityOf<isq::area> auto area)
{
  return mass / area;
}

int main()
{
  constexpr Unit auto cm2 = cm * cm;
  quantity mass = 350. * g;         // e.g., snow load on a small roof patch
  quantity area = 5000. * cm2;      // 0.5 square meters
  auto res = surface_mass_density(mass, area);
  std::cout << res << '\n';
}

Takeaways

  • Use C++ concepts to enforce that only valid mass and area quantities can be passed to the function, ensuring type safety at compile time.
  • The refactored function is generic and works with any compatible units or prefixes (e.g., g, kg, m², cm²), making it robust and flexible.
  • The concept in the return type both advertises the output quantity and verifies that the quantity equation inside the function is correct (i.e., the result is a valid quantity type for surface mass density).
  • The return type is automatically deduced by the type system, reflecting the correct derived unit (e.g., g/cm², kg/m²).
  • Compile-time errors will occur if you attempt to pass incompatible types (e.g., length instead of mass, or volume instead of area) or perform incorrect calculation inside of the function (e.g., multiplication instead of division), preventing subtle bugs.
  • This approach improves runtime performance and reduces the risk of data truncation during unnecessary conversions.