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:
-
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.
-
Implement compile-time arithmetic using template metaprogramming to compute derived dimensions (velocity = distance / time) at compile time with zero runtime overhead.
-
Apply SFINAE (Substitution Failure Is Not An Error) and
if constexprto conditionally enable operations based on type properties, creating intuitive compile-time error messages. -
Use
std::ratiofor representing fractional unit conversions (kilometers to meters, hours to seconds) with exact compile-time arithmetic avoiding floating-point errors. -
Design operator overloading for templated types, ensuring that operations like multiplication and division produce correctly typed results while addition and subtraction enforce matching dimensions.
-
Understand the philosophy of zero-overhead abstractions - safety and expressiveness without any runtime cost.
-
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:
- Performance: Every operation needs a unit check
- Coverage: Bugs only appear when the code path is executed
- 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:
- Represents physical dimensions as template parameters (mass M, length L, time T)
- Stores magnitudes using double (or templated on representation type)
- Enforces dimensional compatibility at compile time
- Derives new dimensions from arithmetic operations
- 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 dimensionRatiodefaults tostd::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:
-
A reusable library that you can use in physics simulations, game engines, or any application with physical quantities
- Deep understanding of TMP that transfers to other domains:
- Type-safe database query builders
- Compile-time validation of configuration
- Zero-overhead design patterns
- 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:
- What is the difference between a template parameter and a function parameter?
- Can you explain template specialization vs partial specialization?
- What is a “type alias” and how does
usingdiffer fromtypedef?
Type Traits:
- What is
std::is_same<T, U>and when would you use it? - How does
std::enable_ifwork? What is SFINAE? - What is
if constexprand how does it differ from regularif?
std::ratio:
- How does
std::ratio<3, 4>represent the fraction 3/4? - What is
std::ratio_multiplyand when is it useful? - How can you compare two ratios at compile time?
5.5 Questions to Guide Your Design
Answer these before writing code:
Dimension Representation:
- Should dimensions use signed or unsigned integers for exponents?
- How will you handle dimensionless quantities (scalar multiplication)?
- What if someone needs more than M, L, T (e.g., electric current)?
Operator Design:
- Should
operator+return the same type as its operands, or a potentially different type? - How should you handle mixed ratios (e.g., meters + kilometers)?
- Should multiplication of a quantity by a scalar be supported? Which scalar types?
Error Messages:
- How can you make compile errors readable when dimensions mismatch?
- Should you use
static_assertwith custom messages, or rely on SFINAE?
Conversions:
- When should implicit conversion be allowed?
- 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:
- What is the dimension of
velocity? - What is the ratio of
velocity? - What is
velocity.count()? - 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:
- “Explain how your unit library achieves zero runtime overhead.”
- Expected: Explanation of compile-time template instantiation, how only doubles remain at runtime
- “What happens if I try to add meters and seconds in your library?”
- Expected: Compile error, explanation of why the operator+ doesn’t match
- “How does your library handle unit conversion, like kilometers to meters?”
- Expected: Explanation of std::ratio, compile-time scaling factors
- “Can you extend this to handle more dimensions, like electric current?”
- Expected: Yes, add template parameter. Discuss tradeoffs of many parameters.
- “What is SFINAE and how does your library use it?”
- Expected: Substitution Failure Is Not An Error, used to remove invalid operator overloads
- “Compare your approach to std::chrono. What did you learn from it?”
- Expected: chrono handles one dimension (time), yours generalizes to multiple
- “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:
Dimtemplate struct with constexpr membersDimMultiplyandDimDividemetafunctions- Comprehensive
static_asserttests
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:
Quantityclass withcount()methodoperator*andoperator/that produce correct dimension typesoperator+andoperator-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:
- Require explicit conversion (safest)
- Convert to the larger ratio (e.g., result is in km)
- 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_castwork? - How are arithmetic operations implemented?
- How does
std::common_typehelp 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
::typemember and_talias - I can use partial specialization to implement compile-time conditionals
- I can read and write
std::enable_ifexpressions
std::ratio:
- I understand how ratio represents exact fractions
- I can use
ratio_multiply,ratio_divide, andratio_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
- Dimension arithmetic works:
using V = DimDivide_t<Dim<0,1,0>, Dim<0,0,1>>; static_assert(V::length == 1 && V::time == -1); - Quantity operations work:
Meters d(100.0); Seconds t(10.0); auto v = d / t; // Compiles // auto bad = d + t; // Does NOT compile - Unit conversions work:
Kilometers km(1.0); Meters m = km; assert(m.count() == 1000.0);
Full Implementation
-
All SI base units defined: Meters, Kilograms, Seconds, and scaled variants
- 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 - Comparison operators:
Meters a(100.0), b(50.0); assert(a > b); - Clear error messages: Dimension mismatch produces an understandable static_assert failure
Excellence Criteria
- User-defined literals:
auto d = 100.0_m; auto t = 10.0_s; -
constexpr throughout: All operations work at compile time
-
Documentation: README with examples, API reference
- 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