Skip to content

Average Speed: Representation Types and Conversions

Try it live on Compiler Explorer

Overview

This example builds on hello_units by exploring different API design choices when working with quantities. It demonstrates how fixed-unit functions compare to generic quantity functions, and shows the impact of unit conversions and representation types on precision and performance.

Key Features Demonstrated

  • Fixed-unit vs. generic quantity function interfaces
  • Impact of representation types (int vs. double)
  • Value-preserving and value-truncating conversions
  • Working with multiple unit systems (SI, CGS, International)
  • Precision loss during unit conversions

Code Walkthrough

Including Headers and Namespaces

First, we either import a module or include all the necessary header files and import all the identifiers from the mp_units namespace:

avg_speed.cpp
#ifdef MP_UNITS_IMPORT_STD
import std;
#else
#include <exception>
#include <iostream>
#endif
#ifdef MP_UNITS_MODULES
import mp_units;
#else
#include <mp-units/systems/cgs.h>
#include <mp-units/systems/international.h>
#include <mp-units/systems/isq.h>
#include <mp-units/systems/si.h>
#endif

namespace {

using namespace mp_units;

Function Definitions

Next, we define two functions calculating average speed based on quantities of fixed units and integral and floating-point representation types, respectively, and a third function that we introduced in the previous example:

avg_speed.cpp
constexpr quantity<si::metre / si::second, int> fixed_int_si_avg_speed(quantity<si::metre, int> d,
                                                                       quantity<si::second, int> t)
{
  return d / t;
}

constexpr quantity<si::metre / si::second> fixed_double_si_avg_speed(quantity<si::metre> d, quantity<si::second> t)
{
  return d / t;
}

constexpr QuantityOf<isq::speed> auto avg_speed(QuantityOf<isq::length> auto d, QuantityOf<isq::time> auto t)
{
  return d / t;
}

We also added a simple utility to print our results:

avg_speed.cpp
template<QuantityOf<isq::length> D, QuantityOf<isq::time> T, QuantityOf<isq::speed> V>
void print_result(D distance, T duration, V speed)
{
  const auto result_in_kmph = speed.force_in(si::kilo<si::metre> / non_si::hour);
  std::cout << "Average speed of a car that makes " << distance << " in " << duration << " is " << result_in_kmph
            << ".\n";
}

Comparing Function Behavior

Now, let's analyze how those three utility functions behave with different sets of arguments.

SI Units with Integral Representation

First, we are going to use quantities of SI units and integral representation:

avg_speed.cpp
void example()
{
  using namespace mp_units::si::unit_symbols;

  // SI (int)
  {
    constexpr auto distance = 220 * km;
    constexpr auto duration = 2 * h;

    std::cout << "SI units with 'int' as representation\n";

    print_result(distance, duration, fixed_int_si_avg_speed(distance, duration));
    print_result(distance, duration, fixed_double_si_avg_speed(distance, duration));
    print_result(distance, duration, avg_speed(distance, duration));
  }

The above provides the following output:

SI units with 'int' as representation
Average speed of a car that makes 220 km in 2 h is 108 km/h.
Average speed of a car that makes 220 km in 2 h is 110 km/h.
Average speed of a car that makes 220 km in 2 h is 110 km/h.

Please note that in the first two cases, we must convert length from km to m and time from h to s. The converted values are used to calculate speed in m/s which is then again converted to the one in km/h. Those conversions not only impact the application's runtime performance but may also affect the precision of the final result. Such truncation can be easily observed in the first case where we deal with integral representation types (the resulting speed is \(108\ \mathrm{km/h}\)).

The second scenario is really similar to the previous one, but this time, function arguments have floating-point representation types:

avg_speed.cpp
  // SI (double)
  {
    constexpr auto distance = 220. * km;
    constexpr auto duration = 2. * h;

    std::cout << "\nSI units with 'double' as representation\n";

    // conversion from a floating-point to an integral type is a truncating one so an explicit cast is needed
    print_result(distance, duration, fixed_int_si_avg_speed(value_cast<int>(distance), value_cast<int>(duration)));
    print_result(distance, duration, fixed_double_si_avg_speed(distance, duration));
    print_result(distance, duration, avg_speed(distance, duration));
  }

Conversion from floating-point to integral representation types is considered value-truncating and that is why now, in the first case, we need an explicit call to value_cast<int>.

In the text output, we can observe that, again, the resulting value gets truncated during conversions in the first cast:

SI units with 'double' as representation
Average speed of a car that makes 220 km in 2 h is 108 km/h.
Average speed of a car that makes 220 km in 2 h is 110 km/h.
Average speed of a car that makes 220 km in 2 h is 110 km/h.

International Mile Units

Next, let's do the same for integral and floating-point representations, but this time using international mile:

avg_speed.cpp
  // International mile (int)
  {
    using namespace mp_units::international::unit_symbols;

    constexpr auto distance = 140 * mi;
    constexpr auto duration = 2 * h;

    std::cout << "\nInternational mile with 'int' as representation\n";

    // it is not possible to make a lossless conversion of miles to meters on an integral type
    // (explicit cast needed)
    print_result(distance, duration, fixed_int_si_avg_speed(distance.force_in(m), duration));
    print_result(distance, duration, fixed_double_si_avg_speed(distance, duration));
    print_result(distance, duration, avg_speed(distance, duration));
  }

  // International mile (double)
  {
    using namespace mp_units::international::unit_symbols;

    constexpr auto distance = 140. * mi;
    constexpr auto duration = 2. * h;

    std::cout << "\nInternational mile with 'double' as representation\n";

    // conversion from a floating-point to an integral type is a truncating one so an explicit cast is needed
    // also it is not possible to make a lossless conversion of miles to meters on an integral type
    // (explicit cast needed)
    print_result(distance, duration, fixed_int_si_avg_speed(value_cast<m, int>(distance), value_cast<int>(duration)));
    print_result(distance, duration, fixed_double_si_avg_speed(distance, duration));
    print_result(distance, duration, avg_speed(distance, duration));
  }

One important difference here is the fact that as it is not possible to make a lossless conversion of miles to meters on a quantity using an integral representation type, so this time, we need a value_cast<m, int> to force it.

If we check the text output of the above, we will see the following:

International mile with 'int' as representation
Average speed of a car that makes 140 mi in 2 h is 111 km/h.
Average speed of a car that makes 140 mi in 2 h is 112.654 km/h.
Average speed of a car that makes 140 mi in 2 h is 112 km/h.

International mile with 'double' as representation
Average speed of a car that makes 140 mi in 2 h is 111 km/h.
Average speed of a car that makes 140 mi in 2 h is 112.654 km/h.
Average speed of a car that makes 140 mi in 2 h is 112.654 km/h.

Please note how the first and third results get truncated using integral representation types.

CGS Units

In the end, we repeat the scenario for CGS units:

avg_speed.cpp
  // CGS (int)
  {
    constexpr auto distance = 22'000'000 * cgs::centimetre;
    constexpr auto duration = 7200 * cgs::second;

    std::cout << "\nCGS units with 'int' as representation\n";

    // it is not possible to make a lossless conversion of centimeters to meters on an integral type
    // (explicit cast needed)
    print_result(distance, duration, fixed_int_si_avg_speed(distance.force_in(m), duration));
    print_result(distance, duration, fixed_double_si_avg_speed(distance, duration));
    print_result(distance, duration, avg_speed(distance, duration));
  }

  // CGS (double)
  {
    constexpr auto distance = 22'000'000. * cgs::centimetre;
    constexpr auto duration = 7200. * cgs::second;

    std::cout << "\nCGS units with 'double' as representation\n";

    // conversion from a floating-point to an integral type is a truncating one so an explicit cast is needed
    // it is not possible to make a lossless conversion of centimeters to meters on an integral type
    // (explicit cast needed)
    print_result(distance, duration, fixed_int_si_avg_speed(value_cast<m, int>(distance), value_cast<int>(duration)));

    print_result(distance, duration, fixed_double_si_avg_speed(distance, duration));
    print_result(distance, duration, avg_speed(distance, duration));
  }
}

}  // namespace

Again, we observe value_cast being used in the same places and consistent truncation errors in the text output:

CGS units with 'int' as representation
Average speed of a car that makes 22000000 cm in 7200 s is 108 km/h.
Average speed of a car that makes 22000000 cm in 7200 s is 110 km/h.
Average speed of a car that makes 22000000 cm in 7200 s is 109 km/h.

CGS units with 'double' as representation
Average speed of a car that makes 2.2e+07 cm in 7200 s is 108 km/h.
Average speed of a car that makes 2.2e+07 cm in 7200 s is 110 km/h.
Average speed of a car that makes 2.2e+07 cm in 7200 s is 110 km/h.

The example file ends with a simple main() function:

avg_speed.cpp
int main()
{
  try {
    example();
  } catch (const std::exception& ex) {
    std::cerr << "Unhandled std exception caught: " << ex.what() << '\n';
  } catch (...) {
    std::cerr << "Unhandled unknown exception caught\n";
  }
}

Key Takeaways

API Design Trade-offs

  1. Fixed-Unit Functions (fixed_int_si_avg_speed, fixed_double_si_avg_speed):

    • ✅ Simple and explicit about units
    • ❌ Require runtime unit conversions at call site
    • ❌ May truncate the quantity value
    • ❌ Less flexible for users
  2. Generic Quantity Functions (avg_speed):

    • ✅ Accept any compatible units with no runtime overhead
    • ✅ More convenient for users
    • ✅ Better composability
    • ⚠️ Require C++ templates (e.g., often provided in header files)

Precision Considerations

  • Integral representations: Value-truncating conversions can lose precision
  • Floating-point representations: Better for intermediate conversions
  • Multiple conversions: Each conversion step can accumulate errors
  • Generic functions: Minimize conversions by working in the user's units