Project 3: Compile-Time Unit Conversion Library

Encode physical units in the type system for zero-overhead safety

Quick Reference

Attribute Value
Difficulty Expert
Time Estimate 2-3 weeks
Language C++17 (C++20 optional for concepts)
Prerequisites Strong template knowledge, type traits, variadic templates
Key Topics Template metaprogramming, std::ratio, SFINAE, if constexpr
Main Book “C++ Templates: The Complete Guide, 2nd Edition” by Vandevoorde, Josuttis, and Gregor

1. Learning Objectives

By completing this project, you will be able to:

  1. Design type-safe abstractions that encode domain invariants (physical dimensions) directly into the C++ type system, making invalid operations a compile-time error rather than a runtime bug.

  2. Implement compile-time arithmetic using template metaprogramming to compute derived dimensions (velocity = distance / time) at compile time with zero runtime overhead.

  3. Apply SFINAE (Substitution Failure Is Not An Error) and if constexpr to conditionally enable operations based on type properties, creating intuitive compile-time error messages.

  4. Use std::ratio for representing fractional unit conversions (kilometers to meters, hours to seconds) with exact compile-time arithmetic avoiding floating-point errors.

  5. Design operator overloading for templated types, ensuring that operations like multiplication and division produce correctly typed results while addition and subtraction enforce matching dimensions.

  6. Understand the philosophy of zero-overhead abstractions - safety and expressiveness without any runtime cost.

  7. Apply these techniques to real-world domains beyond physics: currency, database column types, API contracts, and more.


2. Theoretical Foundation

2.1 Core Concepts

Dimensional Analysis: The Mathematics of Physical Quantities

In physics and engineering, every measurement has two components: a magnitude (a number) and a dimension (what kind of thing you’re measuring). The SI system defines seven base dimensions:

Dimension Symbol SI Base Unit
Length L meter (m)
Mass M kilogram (kg)
Time T second (s)
Electric Current I ampere (A)
Temperature Θ kelvin (K)
Amount of Substance N mole (mol)
Luminous Intensity J candela (cd)

Every physical quantity can be expressed as a product of powers of these base dimensions. For example:

Velocity     = L^1 * T^-1           (meters per second)
Acceleration = L^1 * T^-2           (meters per second squared)
Force        = M^1 * L^1 * T^-2     (Newton = kg * m / s^2)
Energy       = M^1 * L^2 * T^-2     (Joule = kg * m^2 / s^2)

The key insight: dimensions follow algebraic rules. When you multiply two quantities, you add their dimension exponents. When you divide, you subtract.

Velocity = Distance / Time
  L^1 * T^0   /   L^0 * T^1   =   L^(1-0) * T^(0-1)   =   L^1 * T^-1

Type-Level Programming: Computation at Compile Time

In C++, templates are not just for generic programming - they’re a complete compile-time programming language. The compiler evaluates template code to produce types and values before your program ever runs.

Values at compile time:

template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N-1>::value;
};

template<>
struct Factorial<0> {
    static constexpr int value = 1;
};

// Computed at compile time!
constexpr int x = Factorial<5>::value;  // x = 120

Types at compile time:

template<typename T, typename U>
struct TypePair {
    using First = T;
    using Second = U;
};

using MyPair = TypePair<int, double>;
using A = MyPair::First;   // A is int
using B = MyPair::Second;  // B is double

The unit library combines both: types encode dimensions, and template arithmetic computes dimension transformations.

The Power of Type-Driven Design

Traditional runtime checking:

// Bad: Errors at runtime, crashes in production
double compute_force(double mass, double acceleration) {
    return mass * acceleration;
}

double distance = 100.0;  // meters
double time = 10.0;       // seconds
double force = compute_force(distance, time);  // WRONG! Compiles fine, crashes logic

Type-driven design:

// Good: Errors at compile time, impossible to ship
template<int M, int L, int T>
struct Value { /* ... */ };

using Mass = Value<1, 0, 0>;
using Acceleration = Value<0, 1, -2>;
using Force = Value<1, 1, -2>;

Force compute_force(Mass m, Acceleration a) {
    return m * a;  // Type system enforces correctness
}

Length distance(100.0);
Time time(10.0);
Force f = compute_force(distance, time);  // COMPILE ERROR!

2.2 Why This Matters

The Mars Climate Orbiter: A $327 Million Bug

In 1999, NASA’s Mars Climate Orbiter was lost because one software component used imperial units (pound-force seconds) while another expected metric units (newton-seconds). The spacecraft entered the atmosphere at the wrong angle and disintegrated.

This wasn’t a careless programmer error - it was a type system failure. If the software had used a units library that encoded dimensions in types, the bug would have been caught at compile time.

Real-World Applications

Scientific Computing:

  • Simulation software (fluid dynamics, structural analysis)
  • Sensor data processing (temperature, pressure, flow rate)
  • Control systems (robotics, aerospace, automotive)

Financial Systems:

  • Currency type safety (USD, EUR, JPY cannot be accidentally mixed)
  • Interest rate calculations (annual vs monthly rates)
  • Risk calculations (variance, standard deviation with correct dimensions)

API Design:

  • Database queries (ensuring column types match operations)
  • Network protocols (byte ordering, packet sizes)
  • Configuration systems (timeout in seconds vs milliseconds)

2.3 Historical Context: std::chrono as Inspiration

C++11 introduced std::chrono, a type-safe time library that demonstrates the power of compile-time unit checking:

#include <chrono>
using namespace std::chrono;

milliseconds ms(1000);
seconds s = ms;  // Implicit conversion: OK, no precision loss

seconds s2(1);
milliseconds ms2 = s2;  // Also OK

// But this is a compile error:
// hours h = 500ms;  // Error: would lose precision!

// Duration arithmetic produces correct types:
auto total = 2h + 30min + 45s;  // total is seconds, computed at compile time

The std::chrono library uses std::ratio to represent the relationship between different time units:

template<typename Rep, typename Period = std::ratio<1>>
class duration {
    Rep rep_;
public:
    // Period::num / Period::den is the conversion factor
};

using milliseconds = duration<long long, std::ratio<1, 1000>>;
using seconds = duration<long long, std::ratio<1, 1>>;
using minutes = duration<long long, std::ratio<60, 1>>;
using hours = duration<long long, std::ratio<3600, 1>>;

Your unit library will generalize this approach from one dimension (time) to multiple dimensions (mass, length, time, etc.).

2.4 Common Misconceptions

Misconception 1: “Template metaprogramming has runtime overhead”

Reality: Template metaprogramming is pure compile-time computation. The compiled binary contains only the final result - no trace of the template machinery remains.

// At compile time:
Value<0, 1, -1> velocity = distance / time;

// In the final binary:
double velocity = distance.value / time.value;  // Just one division!

Misconception 2: “I can just use runtime unit checking”

Reality: Runtime checking has three problems:

  1. Performance: Every operation needs a unit check
  2. Coverage: Bugs only appear when the code path is executed
  3. Error messages: Errors happen in production, not during development

Compile-time checking has none of these issues.

Misconception 3: “This is too complex for practical use”

Reality: Once the library is built, using it is simpler than not using it:

// Without unit library: careful documentation and hope
double area = length * width;  // Are they both in meters? Better hope so!

// With unit library: self-documenting, self-checking
auto area = length * width;  // Compiler deduces Area type automatically

Misconception 4: “std::chrono already solves this”

Reality: std::chrono handles only time. For multi-dimensional analysis (physics, engineering), you need a library that tracks multiple dimension exponents simultaneously.


3. Project Specification

3.1 What You Will Build

A compile-time unit library that:

  1. Represents physical dimensions as template parameters (mass M, length L, time T)
  2. Stores magnitudes using double (or templated on representation type)
  3. Enforces dimensional compatibility at compile time
  4. Derives new dimensions from arithmetic operations
  5. Converts between unit scales (meters to kilometers, seconds to hours)

3.2 Functional Requirements

FR1: Dimension Representation

  • Represent dimensions as integer exponents: Dim<M, L, T> where M, L, T are ints
  • Support at least Mass (M), Length (L), and Time (T)
  • Dimensionless quantities have all exponents = 0

FR2: Quantity Type

  • Quantity<Dim, Ratio> holds a double value and its dimension
  • Ratio defaults to std::ratio<1> (base units)
  • Provide .count() to access the raw value

FR3: Arithmetic Operations

Operation Dimension Rule Value Rule
a * b Add exponents Multiply values
a / b Subtract exponents Divide values
a + b Must match (compile error otherwise) Add values
a - b Must match (compile error otherwise) Subtract values

FR4: Comparison Operations

  • Comparison only allowed for same dimension
  • ==, !=, <, >, <=, >=

FR5: Unit Definitions

Provide convenient type aliases:

using Meters    = Quantity<Dim<0, 1, 0>, std::ratio<1>>;
using Kilometers = Quantity<Dim<0, 1, 0>, std::ratio<1000>>;
using Seconds   = Quantity<Dim<0, 0, 1>, std::ratio<1>>;
using Hours     = Quantity<Dim<0, 0, 1>, std::ratio<3600>>;
using Kilograms = Quantity<Dim<1, 0, 0>, std::ratio<1>>;

FR6: Derived Units

Automatically derived through operations:

using Velocity     = decltype(Meters{} / Seconds{});
using Acceleration = decltype(Velocity{} / Seconds{});
using Force        = decltype(Kilograms{} * Acceleration{});

FR7: Implicit Conversion

Allow implicit conversion when:

  • Dimensions match
  • No precision loss (e.g., hours to seconds: OK)

Require explicit cast when precision might be lost (e.g., seconds to hours).

3.3 Non-Functional Requirements

NFR1: Zero Runtime Overhead

All dimension checking and conversion factors must be computed at compile time. The generated code should be identical to hand-written double arithmetic.

NFR2: Clear Compile-Time Errors

When operations fail (e.g., adding meters to seconds), the error message should clearly indicate the dimension mismatch.

NFR3: Extensibility

Adding new dimensions (electric current, temperature, etc.) should require minimal changes to the core library.

3.4 Example Usage / Output

#include "units.hpp"
#include <iostream>

int main() {
    // Define quantities with different units
    Kilometers distance(100.0);       // 100 km
    Hours travel_time(2.0);           // 2 hours

    // Division creates velocity (compile-time type deduction)
    auto velocity = distance / travel_time;
    // velocity has dimension L^1 * T^-1

    // Convert to SI base units for output
    Meters d_meters = distance;       // Implicit: 100,000 m
    Seconds t_seconds = travel_time;  // Implicit: 7,200 s

    std::cout << "Distance: " << d_meters.count() << " m\n";
    std::cout << "Time: " << t_seconds.count() << " s\n";
    std::cout << "Velocity: " << velocity.count() << " km/h\n";

    // Type-safe arithmetic
    Meters a(100.0);
    Meters b(50.0);
    auto sum = a + b;  // OK: same dimension
    std::cout << "Sum: " << sum.count() << " m\n";

    // This would NOT compile:
    // auto invalid = distance + travel_time;
    // Error: cannot add Kilometers and Hours (different dimensions)

    // Derived units
    Kilograms mass(75.0);
    auto weight = mass * MetersPerSecondSquared(9.81);  // Force!
    std::cout << "Weight: " << weight.count() << " N\n";

    return 0;
}

Expected Output:

Distance: 100000 m
Time: 7200 s
Velocity: 50 km/h
Sum: 150 m
Weight: 735.75 N

3.5 Real World Outcome

When you complete this project, you’ll have:

  1. A reusable library that you can use in physics simulations, game engines, or any application with physical quantities

  2. Deep understanding of TMP that transfers to other domains:
    • Type-safe database query builders
    • Compile-time validation of configuration
    • Zero-overhead design patterns
  3. Interview-ready knowledge of a classic TMP problem that demonstrates mastery of the C++ type system

4. Solution Architecture

4.1 High-Level Design

                    COMPILE-TIME TYPE SYSTEM
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

           Dim<M, L, T>                 std::ratio<N, D>
         ┌─────────────┐               ┌──────────────────┐
         │ Mass exp: M │               │ Numerator: N     │
         │ Length: L   │               │ Denominator: D   │
         │ Time: T     │               │                  │
         └──────┬──────┘               └────────┬─────────┘
                │                               │
                │        Quantity<Dim, Ratio>   │
                │      ┌───────────────────┐    │
                └─────►│  Dimension type   │◄───┘
                       │  Scale factor     │
                       │  value: double    │
                       └─────────┬─────────┘
                                 │
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                                 │
                    RUNTIME VALUE STORAGE
                                 │
                                 ▼
                       ┌─────────────────┐
                       │ double value_   │
                       │ (the only       │
                       │  runtime data!) │
                       └─────────────────┘

4.2 Key Components

Component 1: Dimension Type (Dim)

Represents the exponents of base dimensions:

        Dim<M, L, T>
    ┌───────────────────────┐
    │ Template Parameters:  │
    │   M = mass exponent   │
    │   L = length exponent │
    │   T = time exponent   │
    └───────────────────────┘

    Examples:
    ┌─────────────────┬──────────┬───────┬───────┬───────┐
    │ Physical Qty    │ Type     │ M     │ L     │ T     │
    ├─────────────────┼──────────┼───────┼───────┼───────┤
    │ Dimensionless   │ Dim<0,0,0> │ 0   │ 0     │ 0     │
    │ Mass            │ Dim<1,0,0> │ 1   │ 0     │ 0     │
    │ Length          │ Dim<0,1,0> │ 0   │ 1     │ 0     │
    │ Time            │ Dim<0,0,1> │ 0   │ 0     │ 1     │
    │ Velocity        │ Dim<0,1,-1>│ 0   │ 1     │ -1    │
    │ Acceleration    │ Dim<0,1,-2>│ 0   │ 1     │ -2    │
    │ Force           │ Dim<1,1,-2>│ 1   │ 1     │ -2    │
    │ Energy          │ Dim<1,2,-2>│ 1   │ 2     │ -2    │
    └─────────────────┴──────────┴───────┴───────┴───────┘

Component 2: Dimension Arithmetic

Template metafunctions that compute new dimensions:

    DimMultiply: Add exponents
    ────────────────────────────────────────────────

    Velocity * Time = Distance

    Dim<0, 1, -1>  *  Dim<0, 0, 1>  =  Dim<0, 1, 0>
         │                │                │
         ▼                ▼                ▼
    ┌─────────┐     ┌─────────┐      ┌─────────┐
    │ M: 0    │     │ M: 0    │      │ M: 0+0  │
    │ L: 1    │  +  │ L: 0    │  =   │ L: 1+0  │
    │ T: -1   │     │ T: 1    │      │ T: -1+1 │
    └─────────┘     └─────────┘      └─────────┘


    DimDivide: Subtract exponents
    ────────────────────────────────────────────────

    Distance / Time = Velocity

    Dim<0, 1, 0>  /  Dim<0, 0, 1>  =  Dim<0, 1, -1>
         │                │                │
         ▼                ▼                ▼
    ┌─────────┐     ┌─────────┐      ┌─────────┐
    │ M: 0    │     │ M: 0    │      │ M: 0-0  │
    │ L: 1    │  -  │ L: 0    │  =   │ L: 1-0  │
    │ T: 0    │     │ T: 1    │      │ T: 0-1  │
    └─────────┘     └─────────┘      └─────────┘

Component 3: Quantity Type

Wraps a value with its dimensional type:

    Quantity<Dim<M, L, T>, Ratio>
    ┌────────────────────────────────────┐
    │ Template:                          │
    │   Dim = dimension type             │
    │   Ratio = unit scale (e.g., 1000   │
    │           for kilometers)          │
    │                                    │
    │ Data:                              │
    │   double value_                    │
    │                                    │
    │ Methods:                           │
    │   count() -> double                │
    │   operator+, -, *, /               │
    │   operator==, <, etc.              │
    └────────────────────────────────────┘

Component 4: SI Base Units and Derived Units

    SI Base Units (all with Ratio = 1)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    ┌───────────────┬──────────────────────────────────┐
    │ Type Alias    │ Definition                       │
    ├───────────────┼──────────────────────────────────┤
    │ Meters        │ Quantity<Dim<0,1,0>, ratio<1>>   │
    │ Kilograms     │ Quantity<Dim<1,0,0>, ratio<1>>   │
    │ Seconds       │ Quantity<Dim<0,0,1>, ratio<1>>   │
    └───────────────┴──────────────────────────────────┘

    Scaled Units (different Ratios)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    ┌───────────────┬──────────────────────────────────┐
    │ Kilometers    │ Quantity<Dim<0,1,0>, ratio<1000>>│
    │ Millimeters   │ Quantity<Dim<0,1,0>, ratio<1,1000>>│
    │ Hours         │ Quantity<Dim<0,0,1>, ratio<3600>>│
    │ Minutes       │ Quantity<Dim<0,0,1>, ratio<60>>  │
    └───────────────┴──────────────────────────────────┘

    Derived Units (computed from operations)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    ┌────────────────────┬───────────────────────────────┐
    │ Velocity (m/s)     │ Meters / Seconds              │
    │                    │ = Quantity<Dim<0,1,-1>, ...>  │
    │                    │                               │
    │ Acceleration (m/s^2)│ Velocity / Seconds           │
    │                    │ = Quantity<Dim<0,1,-2>, ...>  │
    │                    │                               │
    │ Force (N)          │ Kilograms * Acceleration      │
    │                    │ = Quantity<Dim<1,1,-2>, ...>  │
    │                    │                               │
    │ Energy (J)         │ Force * Meters                │
    │                    │ = Quantity<Dim<1,2,-2>, ...>  │
    └────────────────────┴───────────────────────────────┘

4.3 Data Structures

The Dim Template

template<int M, int L, int T>
struct Dim {
    static constexpr int mass = M;
    static constexpr int length = L;
    static constexpr int time = T;
};

Dimension Metafunctions

// Multiply dimensions: add exponents
template<typename D1, typename D2>
struct DimMultiply;

template<int M1, int L1, int T1, int M2, int L2, int T2>
struct DimMultiply<Dim<M1, L1, T1>, Dim<M2, L2, T2>> {
    using type = Dim<M1 + M2, L1 + L2, T1 + T2>;
};

// Divide dimensions: subtract exponents
template<typename D1, typename D2>
struct DimDivide;

template<int M1, int L1, int T1, int M2, int L2, int T2>
struct DimDivide<Dim<M1, L1, T1>, Dim<M2, L2, T2>> {
    using type = Dim<M1 - M2, L1 - L2, T1 - T2>;
};

// Convenience aliases
template<typename D1, typename D2>
using DimMultiply_t = typename DimMultiply<D1, D2>::type;

template<typename D1, typename D2>
using DimDivide_t = typename DimDivide<D1, D2>::type;

The Quantity Template

template<typename Dimension, typename Ratio = std::ratio<1>>
class Quantity {
private:
    double value_;

public:
    using dimension = Dimension;
    using ratio = Ratio;

    constexpr explicit Quantity(double v = 0.0) : value_(v) {}

    constexpr double count() const { return value_; }

    // Operators defined as friend functions...
};

4.4 Algorithm Overview: Compile-Time Dimension Arithmetic

The key insight is that all dimension arithmetic happens through template specialization and type deduction:

    operator* Execution Flow
    ════════════════════════════════════════════════════════════

    Step 1: Call operator*
    ──────────────────────
    Quantity<Dim<0,1,0>> distance(100);  // Meters
    Quantity<Dim<0,0,1>> time(10);       // Seconds
    auto velocity = distance * time;     // Triggers operator*


    Step 2: Compiler deduces return type
    ────────────────────────────────────
    template<typename D1, typename R1, typename D2, typename R2>
    auto operator*(Quantity<D1, R1> lhs, Quantity<D2, R2> rhs) {
        using NewDim = DimMultiply_t<D1, D2>;  // Computed at compile time!
        using NewRatio = std::ratio_multiply<R1, R2>;
        return Quantity<NewDim, NewRatio>(lhs.count() * rhs.count());
    }

    D1 = Dim<0,1,0>    D2 = Dim<0,0,1>
           │                  │
           └───────┬──────────┘
                   ▼
           DimMultiply_t<D1, D2>
                   │
                   ▼
           Dim<0+0, 1+0, 0+1> = Dim<0,1,1>


    Step 3: Compile-time type becomes runtime double
    ─────────────────────────────────────────────────

    What the compiler sees:
        Quantity<Dim<0,1,1>, ratio<1>> velocity =
            Quantity<Dim<0,1,1>, ratio<1>>(100.0 * 10.0);

    What the compiled code does:
        double velocity_value = 100.0 * 10.0;


    operator+ Execution Flow (with type checking)
    ══════════════════════════════════════════════════════════════

    Case 1: Same dimensions (compiles)
    ───────────────────────────────────
    Quantity<Dim<0,1,0>> a(100);
    Quantity<Dim<0,1,0>> b(50);
    auto c = a + b;  // OK: both are Length


    Case 2: Different dimensions (compile error)
    ─────────────────────────────────────────────
    Quantity<Dim<0,1,0>> distance(100);  // Length
    Quantity<Dim<0,0,1>> time(10);       // Time
    auto invalid = distance + time;       // ERROR!

    Why? operator+ is defined as:

    template<typename D, typename R>
    Quantity<D, R> operator+(Quantity<D, R> lhs, Quantity<D, R> rhs) {
        return Quantity<D, R>(lhs.count() + rhs.count());
    }

    Both arguments must have SAME D and SAME R.
    distance has D = Dim<0,1,0>
    time has D = Dim<0,0,1>
    No match! SFINAE removes this overload. No other overload exists.
    Compile error!

5. Implementation Guide

5.1 Development Environment Setup

Required:

  • C++17 compatible compiler (GCC 7+, Clang 5+, MSVC 2017+)
  • CMake 3.12+ (optional but recommended)

Recommended:

# Ubuntu/Debian
sudo apt install g++-11 cmake

# macOS
brew install llvm cmake

# Verify C++17 support
g++ --version  # Should be 7.0 or higher

Compiler Flags:

# Compile with maximum warnings
g++ -std=c++17 -Wall -Wextra -Werror -O2 your_file.cpp

# For seeing template instantiations (debugging)
g++ -std=c++17 -ftemplate-backtrace-limit=0 your_file.cpp

5.2 Project Structure

compile_time_units/
├── CMakeLists.txt
├── include/
│   └── units/
│       ├── dim.hpp           # Dimension type and arithmetic
│       ├── quantity.hpp      # Quantity class with operators
│       ├── si_units.hpp      # SI unit definitions
│       └── units.hpp         # Master header (includes all)
├── src/
│   └── main.cpp              # Demo/example usage
├── tests/
│   ├── test_dim.cpp          # Dimension arithmetic tests
│   ├── test_quantity.cpp     # Quantity operations tests
│   └── test_conversions.cpp  # Unit conversion tests
└── examples/
    ├── physics.cpp           # Physics calculations
    └── custom_units.cpp      # Extending with new dimensions

5.3 The Core Question You’re Answering

“How can we use the C++ type system to encode physical constraints so that invalid operations become impossible at compile time, with zero runtime overhead?”

This question sits at the intersection of:

  • Type theory: Using types to represent more than just data storage
  • Compile-time computation: Moving work from runtime to compile time
  • Zero-overhead abstraction: C++’s fundamental philosophy

5.4 Concepts You Must Understand First

Before implementing, verify you understand these prerequisites:

Template Basics:

  1. What is the difference between a template parameter and a function parameter?
  2. Can you explain template specialization vs partial specialization?
  3. What is a “type alias” and how does using differ from typedef?

Type Traits:

  1. What is std::is_same<T, U> and when would you use it?
  2. How does std::enable_if work? What is SFINAE?
  3. What is if constexpr and how does it differ from regular if?

std::ratio:

  1. How does std::ratio<3, 4> represent the fraction 3/4?
  2. What is std::ratio_multiply and when is it useful?
  3. How can you compare two ratios at compile time?

5.5 Questions to Guide Your Design

Answer these before writing code:

Dimension Representation:

  1. Should dimensions use signed or unsigned integers for exponents?
  2. How will you handle dimensionless quantities (scalar multiplication)?
  3. What if someone needs more than M, L, T (e.g., electric current)?

Operator Design:

  1. Should operator+ return the same type as its operands, or a potentially different type?
  2. How should you handle mixed ratios (e.g., meters + kilometers)?
  3. Should multiplication of a quantity by a scalar be supported? Which scalar types?

Error Messages:

  1. How can you make compile errors readable when dimensions mismatch?
  2. Should you use static_assert with custom messages, or rely on SFINAE?

Conversions:

  1. When should implicit conversion be allowed?
  2. How do you handle precision loss (hours to seconds is OK, but seconds to hours loses precision)?

5.6 Thinking Exercise

Before coding, work through this example by hand:

Exercise: Trace the types through this calculation:

Kilometers distance(100.0);  // 100 km
Hours time(2.0);             // 2 hours
auto velocity = distance / time;  // What is the type?
Meters d_m = distance;       // What conversion happens?

Questions to answer:

  1. What is the dimension of velocity?
  2. What is the ratio of velocity?
  3. What is velocity.count()?
  4. What conversion factor is applied when assigning to d_m?

Work through the template instantiations step by step.

5.7 Hints in Layers

Hint 1: Starting Point

Begin with the simplest possible dimension: just one base dimension.

template<int L>  // Just length for now
struct Dim1D {
    static constexpr int length = L;
};

template<typename D>
class Quantity1D {
    double value_;
public:
    Quantity1D(double v) : value_(v) {}
    double count() const { return value_; }
};

Get operator* working for this simple case first. Once it works, extend to three dimensions.

Hint 2: Next Level

For dimension arithmetic, use template specialization:

template<typename D1, typename D2>
struct DimAdd;  // Primary template (undefined)

template<int L1, int L2>
struct DimAdd<Dim1D<L1>, Dim1D<L2>> {
    using type = Dim1D<L1 + L2>;
};

The key insight: template arithmetic is just matching patterns and producing new types.

Hint 3: Technical Details

For operator+ with same-dimension requirement:

// Method 1: Simple overload (requires exact match)
template<typename D, typename R>
Quantity<D, R> operator+(Quantity<D, R> lhs, Quantity<D, R> rhs) {
    return Quantity<D, R>(lhs.count() + rhs.count());
}

// Method 2: With static_assert for better error messages
template<typename D1, typename R1, typename D2, typename R2>
auto operator+(Quantity<D1, R1> lhs, Quantity<D2, R2> rhs) {
    static_assert(std::is_same_v<D1, D2>,
                  "Cannot add quantities with different dimensions");
    // Handle ratio conversion...
}

Hint 4: Debugging and Verification

Use static_assert liberally to verify your metaprogramming:

using Velocity = DimDivide_t<Dim<0,1,0>, Dim<0,0,1>>;
static_assert(Velocity::mass == 0, "Mass exponent should be 0");
static_assert(Velocity::length == 1, "Length exponent should be 1");
static_assert(Velocity::time == -1, "Time exponent should be -1");

To debug type errors, intentionally create a type mismatch and read the error:

// Diagnostic trick: assign to an incompatible type
int debug = velocity;  // Error message will show velocity's actual type

5.8 The Interview Questions They’ll Ask

After completing this project, be ready to answer:

  1. “Explain how your unit library achieves zero runtime overhead.”
    • Expected: Explanation of compile-time template instantiation, how only doubles remain at runtime
  2. “What happens if I try to add meters and seconds in your library?”
    • Expected: Compile error, explanation of why the operator+ doesn’t match
  3. “How does your library handle unit conversion, like kilometers to meters?”
    • Expected: Explanation of std::ratio, compile-time scaling factors
  4. “Can you extend this to handle more dimensions, like electric current?”
    • Expected: Yes, add template parameter. Discuss tradeoffs of many parameters.
  5. “What is SFINAE and how does your library use it?”
    • Expected: Substitution Failure Is Not An Error, used to remove invalid operator overloads
  6. “Compare your approach to std::chrono. What did you learn from it?”
    • Expected: chrono handles one dimension (time), yours generalizes to multiple
  7. “What are the limitations of this approach?”
    • Expected: Template bloat, compile time, error message readability, only handles linear units

5.9 Books That Will Help

Topic Book Specific Chapters
Template basics “C++ Templates: The Complete Guide, 2nd Ed” Ch 1-5 (Basics)
Template metaprogramming “C++ Templates: The Complete Guide, 2nd Ed” Ch 8 (Compile-Time Programming)
SFINAE “C++ Templates: The Complete Guide, 2nd Ed” Ch 15 (Template Argument Deduction)
Type traits “C++ Templates: The Complete Guide, 2nd Ed” Ch 19 (Type Traits)
std::ratio “The C++ Standard Library, 2nd Ed” by Josuttis Ch 5.4 (Ratio)
std::chrono design “The C++ Standard Library, 2nd Ed” by Josuttis Ch 5.7 (Chrono)
Modern C++ idioms “Effective Modern C++” by Scott Meyers Items 9-13 (auto, type deduction)

5.10 Implementation Phases

Phase 1: Dimension Type (Days 1-2)

Goal: Implement Dim<M, L, T> and dimension arithmetic metafunctions.

Deliverables:

  • Dim template struct with constexpr members
  • DimMultiply and DimDivide metafunctions
  • Comprehensive static_assert tests

Verification:

using D1 = Dim<1, 2, -1>;  // kg * m^2 / s
using D2 = Dim<0, 1, 1>;   // m * s
using Product = DimMultiply_t<D1, D2>;  // kg * m^3

static_assert(Product::mass == 1);
static_assert(Product::length == 3);
static_assert(Product::time == 0);

Phase 2: Basic Quantity (Days 3-5)

Goal: Implement Quantity<Dim> with basic operations (no Ratio yet).

Deliverables:

  • Quantity class with count() method
  • operator* and operator/ that produce correct dimension types
  • operator+ and operator- that require matching dimensions

Verification:

Quantity<Dim<0,1,0>> distance(100.0);  // meters
Quantity<Dim<0,0,1>> time(10.0);       // seconds
auto velocity = distance / time;
static_assert(std::is_same_v<decltype(velocity)::dimension, Dim<0,1,-1>>);

Phase 3: Unit Scaling with std::ratio (Days 6-8)

Goal: Add Ratio parameter for different unit scales.

Deliverables:

  • Extended Quantity<Dim, Ratio> template
  • Ratio multiplication/division in operators
  • Implicit conversion when dimensions match

Verification:

Quantity<Dim<0,1,0>, std::ratio<1000>> km(1.0);  // 1 km
Quantity<Dim<0,1,0>, std::ratio<1>> m = km;      // converts to 1000 m
assert(m.count() == 1000.0);

Phase 4: SI Unit Definitions (Days 9-10)

Goal: Create convenient type aliases for common units.

Deliverables:

  • Base units: Meters, Kilograms, Seconds
  • Scaled units: Kilometers, Grams, Milliseconds
  • Derived units: Velocity, Acceleration, Force, Energy

Verification:

Meters d(100.0);
Seconds t(10.0);
auto v = d / t;  // v has type compatible with Velocity

Phase 5: Comparison and Refinement (Days 11-14)

Goal: Complete the library with comparisons, better errors, and documentation.

Deliverables:

  • Comparison operators (==, <, etc.)
  • Clear error messages using static_assert
  • User documentation and examples
  • Comprehensive test suite

Verification: Full demo program compiles and runs correctly.

5.11 Key Implementation Decisions

Decision 1: Signed vs Unsigned Dimension Exponents

Use signed integers. Negative exponents are essential (velocity = m/s = m^1 * s^-1).

Decision 2: Number of Dimensions

Start with three (M, L, T) for simplicity. Design so that adding more is a straightforward extension:

// Current: 3 dimensions
template<int M, int L, int T>
struct Dim { ... };

// Future: 7 dimensions (full SI)
template<int M, int L, int T, int I, int Theta, int N, int J>
struct Dim { ... };

Decision 3: Value Type

Use double for simplicity. A production library might template on the value type:

template<typename Dimension, typename Ratio, typename Rep = double>
class Quantity {
    Rep value_;
    // ...
};

Decision 4: Handling Mixed Ratios in Addition

When adding quantities with different ratios (e.g., km + m), options:

  1. Require explicit conversion (safest)
  2. Convert to the larger ratio (e.g., result is in km)
  3. Convert to the smaller ratio (e.g., result is in m)

Recommendation: Convert to the smaller ratio (higher precision), matching std::chrono behavior.


6. Testing Strategy

Unit Tests

Dimension Arithmetic Tests

// Test dimension multiplication
static_assert(std::is_same_v<
    DimMultiply_t<Dim<1,0,0>, Dim<0,1,0>>,
    Dim<1,1,0>
>);

// Test dimension division
static_assert(std::is_same_v<
    DimDivide_t<Dim<0,1,0>, Dim<0,0,1>>,
    Dim<0,1,-1>
>);

// Test identity
static_assert(std::is_same_v<
    DimMultiply_t<Dim<1,2,3>, Dim<0,0,0>>,
    Dim<1,2,3>
>);

Quantity Operation Tests

TEST(Quantity, MultiplicationProducesCorrectDimension) {
    Quantity<Dim<1,0,0>> mass(10.0);      // kg
    Quantity<Dim<0,1,-2>> accel(9.81);    // m/s^2
    auto force = mass * accel;

    // Check dimension at compile time
    static_assert(std::is_same_v<decltype(force)::dimension, Dim<1,1,-2>>);

    // Check value at runtime
    EXPECT_DOUBLE_EQ(force.count(), 98.1);
}

TEST(Quantity, AdditionRequiresSameDimension) {
    Quantity<Dim<0,1,0>> a(100.0);
    Quantity<Dim<0,1,0>> b(50.0);
    auto c = a + b;

    EXPECT_DOUBLE_EQ(c.count(), 150.0);
}

// This should NOT compile - verify with a static test
// TEST(Quantity, AdditionFailsForDifferentDimensions) {
//     Quantity<Dim<0,1,0>> distance(100.0);
//     Quantity<Dim<0,0,1>> time(10.0);
//     auto invalid = distance + time;  // Should be compile error
// }

Conversion Tests

TEST(Conversion, KilometersToMeters) {
    Quantity<Dim<0,1,0>, std::ratio<1000>> km(1.0);
    Quantity<Dim<0,1,0>, std::ratio<1>> m = km;

    EXPECT_DOUBLE_EQ(m.count(), 1000.0);
}

TEST(Conversion, HoursToSeconds) {
    Quantity<Dim<0,0,1>, std::ratio<3600>> hours(2.0);
    Quantity<Dim<0,0,1>, std::ratio<1>> seconds = hours;

    EXPECT_DOUBLE_EQ(seconds.count(), 7200.0);
}

Compile-Time Tests

Create a file that should NOT compile if the library is correct:

// test_compile_errors.cpp
// This file should fail to compile. Build system should verify failure.

#include "units.hpp"

void test_dimension_mismatch() {
    Meters distance(100.0);
    Seconds time(10.0);
    auto invalid = distance + time;  // MUST fail
}

Property-Based Testing

// For any quantities a, b, c of compatible dimensions:
// (a + b) + c == a + (b + c)  (associativity)
// a + b == b + a              (commutativity)
// a * (b + c) == a*b + a*c    (distributivity, when types allow)

7. Common Pitfalls and Debugging

Pitfall 1: Template Recursion Depth

Symptom: Compiler error about “template instantiation depth exceeded”

Cause: Template metafunction defined incorrectly, causing infinite recursion

Fix: Ensure base cases are properly specialized:

// BAD: No base case
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N-1>::value;
};
// Factorial<0> would recurse forever!

// GOOD: With base case
template<>
struct Factorial<0> {
    static constexpr int value = 1;
};

Quick verification: Test with small, known values before using in larger computations.

Pitfall 2: Ratio Arithmetic Overflow

Symptom: Strange values, especially with very large or very small ratios

Cause: Integer overflow in ratio numerator/denominator

Fix: std::ratio automatically reduces fractions, but very extreme ratios can still overflow. Use std::ratio_multiply and friends which handle reduction:

// Potentially problematic:
using Big = std::ratio<1000000000, 1>;
using Small = std::ratio<1, 1000000000>;
using Product = std::ratio<Big::num * Small::num, Big::den * Small::den>;  // Overflow!

// Safe:
using Product = std::ratio_multiply<Big, Small>;  // Compiler handles reduction

Quick verification: static_assert(Product::num == expected && Product::den == expected);

Pitfall 3: SFINAE Silent Failure

Symptom: Operator doesn’t exist instead of giving a helpful error

Cause: SFINAE removes the operator silently when types don’t match

Fix: Add a fallback overload with static_assert:

// Version 1: SFINAE (silent failure)
template<typename D, typename R>
auto operator+(Quantity<D,R> a, Quantity<D,R> b) { ... }

// Version 2: Better errors
template<typename D1, typename R1, typename D2, typename R2>
auto operator+(Quantity<D1,R1> a, Quantity<D2,R2> b) {
    static_assert(std::is_same_v<D1, D2>,
        "Cannot add quantities with different dimensions. "
        "Did you mean to multiply or divide instead?");
    // Rest of implementation...
}

Pitfall 4: Missing constexpr

Symptom: Compilation works but values aren’t computed at compile time

Cause: Missing constexpr on functions or constructors

Fix: Add constexpr throughout:

template<typename D, typename R>
class Quantity {
    double value_;
public:
    constexpr explicit Quantity(double v = 0.0) : value_(v) {}  // constexpr!
    constexpr double count() const { return value_; }           // constexpr!
};

template<typename D1, typename R1, typename D2, typename R2>
constexpr auto operator*(Quantity<D1,R1> a, Quantity<D2,R2> b) {  // constexpr!
    // ...
}

Quick verification:

constexpr Meters m(100.0);
constexpr auto m2 = m * m;  // If this compiles, it's constexpr
static_assert(m2.count() == 10000.0);  // Verify at compile time

Pitfall 5: Copy vs Move in Operators

Symptom: Unnecessary copies, poor performance with large quantities

Cause: Passing quantities by value when reference would suffice

Fix: Use const& for operators since Quantity just wraps a double:

// Fine for Quantity<> since it's just a double wrapper:
template<typename D, typename R>
constexpr Quantity<D,R> operator+(Quantity<D,R> a, Quantity<D,R> b);

// Also fine (and more explicit):
template<typename D, typename R>
constexpr Quantity<D,R> operator+(const Quantity<D,R>& a, const Quantity<D,R>& b);

For a templated Rep type, use forwarding references in a production library.


8. Extensions and Challenges

Extension 1: All Seven SI Base Dimensions

Extend to the full SI system:

template<int Mass, int Length, int Time, int Current, int Temperature,
         int Amount, int Luminosity>
struct Dim7 {
    // ...
};

// Electric charge: Ampere * Second
using Coulombs = Quantity<Dim7<0,0,1,1,0,0,0>>;

Challenge: How do you maintain usability with 7 template parameters?

Extension 2: Affine Quantities (Temperature)

Celsius and Fahrenheit are not ratio-scalable - they have offsets. Implement an AffineQuantity that handles conversions like:

Fahrenheit = Celsius * 9/5 + 32

Challenge: How do you represent the offset at compile time?

Extension 3: Dimensional Exponents as std::ratio

For fractional exponents (e.g., square root of area = length):

template<typename M, typename L, typename T>  // Each is std::ratio
struct FracDim { ... };

using SquareRoot_of_Area = FracDim<std::ratio<0>, std::ratio<1>, std::ratio<0>>;
// Wait, that's just Length. But what about:

// Sqrt(meter^3 / second) = meter^(3/2) / second^(1/2)
using SqrtM3PerS = FracDim<std::ratio<0>, std::ratio<3,2>, std::ratio<-1,2>>;

Challenge: How do you check equality of fractional dimensions?

Extension 4: User-Defined Literals

constexpr Meters operator""_m(long double v) {
    return Meters(static_cast<double>(v));
}

auto distance = 100.0_m;  // Nice!

Implement literals for all your units.

Extension 5: Integration with Existing Libraries

Make your quantities work with:

  • Eigen (linear algebra)
  • std::complex
  • JSON serialization

Challenge: How do you serialize the dimension type?

Extension 6: Compile-Time Unit String

Generate unit strings at compile time:

template<typename D>
constexpr auto unit_string() {
    // Returns "m/s" for Velocity, "kg*m/s^2" for Force, etc.
}

Challenge: Compile-time string concatenation and conditional formatting.


9. Real-World Connections

Boost.Units

The Boost.Units library is a mature, production-quality implementation of dimensional analysis in C++. Compare your design to theirs:

// Boost.Units
using namespace boost::units;
quantity<si::length> d(100.0 * si::meters);
quantity<si::time> t(10.0 * si::seconds);
quantity<si::velocity> v = d / t;

Study questions:

  • How does Boost handle many dimensions without template parameter explosion?
  • How does Boost handle affine units (temperature)?
  • What error messages does Boost produce?

mp-units (C++23 and Beyond)

The mp-units library is being considered for standardization. It uses C++20 features like concepts:

// mp-units style
auto distance = 100 * m;
auto time = 10 * s;
auto velocity = distance / time;  // Automatically typed as m/s

Study questions:

  • How do concepts improve error messages?
  • How does mp-units handle unit systems beyond SI?

std::chrono Deep Dive

Read the <chrono> header in your standard library implementation. It’s the canonical example of this pattern:

template<class Rep, class Period = ratio<1>>
class duration {
    Rep rep_;
public:
    // ...
};

Study questions:

  • How does duration_cast work?
  • How are arithmetic operations implemented?
  • How does std::common_type help with mixed operations?

Gaming and Simulation

Game engines like Unreal use similar patterns for type-safe positions, rotations, and physics:

// Unreal-style
FVector Position(100.f, 200.f, 0.f);  // Centimeters
FRotator Rotation(0.f, 45.f, 0.f);    // Degrees

// Type system prevents adding position to rotation
// auto invalid = Position + Rotation;  // Won't compile

Financial Systems

Currency and monetary calculations benefit from the same patterns:

template<char... Currency>  // "USD", "EUR", etc.
class Money {
    int64_t cents_;  // Store as cents to avoid floating point
public:
    // Cannot add USD + EUR without explicit conversion
};

10. Resources

Primary References

Resource Use For
“C++ Templates: The Complete Guide, 2nd Ed” Everything template-related
“Effective Modern C++” Modern C++ idioms
cppreference.com - std::ratio Ratio library reference
cppreference.com - std::chrono Chrono as inspiration

Papers and Articles

Resource Description
P1935: A C++ Approach to Physical Units Standardization proposal
Barton & Nackman’s Dimensional Analysis Original 1994 paper
Type-Safe Physical Computations in C++ Dr. Dobb’s article

Libraries to Study

Library URL Notes
Boost.Units boost.org/libs/units Production-quality
mp-units github.com/mpusz/mp-units Modern C++20/23
nholthaus/units github.com/nholthaus/units Header-only, C++14

Video Resources

Video Description
“Physical Units in C++” - Mateusz Pusz, CppCon mp-units library author
“Template Metaprogramming” - Walter Brown Deep TMP techniques
“The Design of std::chrono” - Howard Hinnant From the library author

11. Self-Assessment Checklist

Dimension System:

  • I can explain why dimensional analysis requires tracking exponents
  • I can derive the dimension of any physical quantity (velocity, force, energy)
  • I understand why negative exponents represent division
  • I can extend my system to new base dimensions

Template Metaprogramming:

  • I can write a template metafunction that computes a type
  • I understand the difference between ::type member and _t alias
  • I can use partial specialization to implement compile-time conditionals
  • I can read and write std::enable_if expressions

std::ratio:

  • I understand how ratio represents exact fractions
  • I can use ratio_multiply, ratio_divide, and ratio_equal
  • I understand when ratio arithmetic can overflow

Library Design:

  • My library produces zero runtime overhead (verified with assembly output)
  • Dimension mismatches produce clear compile-time errors
  • Unit conversions happen automatically when safe
  • I can add new units without modifying the core library

Testing:

  • I have compile-time tests (static_assert)
  • I have runtime tests (Google Test or similar)
  • I have verified that incorrect code fails to compile

12. Submission / Completion Criteria

Your project is complete when:

Minimum Viable Product

  1. Dimension arithmetic works:
    using V = DimDivide_t<Dim<0,1,0>, Dim<0,0,1>>;
    static_assert(V::length == 1 && V::time == -1);
    
  2. Quantity operations work:
    Meters d(100.0);
    Seconds t(10.0);
    auto v = d / t;  // Compiles
    // auto bad = d + t;  // Does NOT compile
    
  3. Unit conversions work:
    Kilometers km(1.0);
    Meters m = km;
    assert(m.count() == 1000.0);
    

Full Implementation

  1. All SI base units defined: Meters, Kilograms, Seconds, and scaled variants

  2. Derived units work:
    auto force = Kilograms(10.0) * (Meters(10.0) / (Seconds(1.0) * Seconds(1.0)));
    // force has dimension M^1 * L^1 * T^-2
    
  3. Comparison operators:
    Meters a(100.0), b(50.0);
    assert(a > b);
    
  4. Clear error messages: Dimension mismatch produces an understandable static_assert failure

Excellence Criteria

  1. User-defined literals:
    auto d = 100.0_m;
    auto t = 10.0_s;
    
  2. constexpr throughout: All operations work at compile time

  3. Documentation: README with examples, API reference

  4. Comprehensive tests: Both compile-time and runtime tests

“If it compiles, the physics is right.”


Project created for: Learn Advanced C++: From Concurrency to Coroutines Last updated: 2025-12-29