98 / 163 · C++20 · 14 min
Dimensional types: prevent adding length and time at compile time
Put the exponents of length and time into the type; numeric values are still handled by ordinary arithmetic. This small model demonstrates that addition requires matching dimensions, that multiplication combines dimensions, and that dimensions, unit scales, and numeric safety are clearly distinguished.
In this lesson
C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond
Actually finished reading 3.1–3.7 (including exercises) of InformIT’s official Chapter 3, A Deeper Look at Metafunctions, and read the corresponding body text of the Boost MPL tutorial on dimensions, quantities, addition/subtraction, multiplication, and division. Chapters 1–2, 4–11 and the appendices were not read; Boost documentation is not the whole book, and the publisher’s purchased edition was not obtained.
Edition, actual reading range, and original sources →Which information belongs to the type, which to the numeric value
Two lengths can be added; a length and a duration cannot. Storing both as double leaves an operation that should be rejected at the interface to human memory. Quantity<L,T> fixes the exponents of length and time into the type—for example length is <1,0>, speed is <1,-1>, duration is <0,1>; the member value holds the actual numeric value.
Chapter 3 of the original book uses MPL integer sequences to express a more complete dimensional space. This lesson deliberately keeps only two dimensions, to avoid wrapping a demonstration of a principle into a general physical-units library. The template computes the result type and does not require the input values to be compile-time constants.
Addition matches; multiplication composes
The addition template’s two parameters use the same L and T. If the two arguments carry different dimensions, template argument deduction cannot obtain consistent parameters, so there is no matching addition. Multiplication receives two sets of exponents and adds the exponents separately: speed times time yields length. Runtime code only needs to multiply the two values; dimensional metadata occupies no storage in each object.
The static assertions on concept Addable make “what is not allowed” part of the example as well: length can be added to length, but not to duration. Actual execution only performs legal operations; there is no deliberate undefined behavior. Implicit conversion back to double is not implemented here, because casually dropping the dimensional tag would make the type boundary a dead letter.
Dimensional correctness does not mean the computation is fully correct
This example assumes numeric values in meters and seconds, but the type does not encode the ratio of meters to kilometers. If a caller mistakenly fills in 3 kilometers as value=3, the compiler cannot detect it; a complete units library also needs scale conversion and an explicit construction interface. Offset units such as Celsius cannot be solved by exponent multiplication alone.
Rounding, infinities, and division-by-zero risks of double exist independently as well. The example chooses small integer results that are exactly representable, so assertions do not depend on fragile floating-point equality. The type system prevents errors of dimensional category; it is not responsible for proving the entire physical model. This example uses C++20 requires to show the rejection boundary; the book’s C++03 MPL syntax is historical implementation background, not a framework that must be hand-rebuilt today.
Pitfalls
- The same dimensions do not mean the same unit scale; this example has no automatic conversion between kilometers and meters.
- A correct type can still overflow or lose precision; compile-time metadata cannot replace numeric analysis.
Run an example
Minimum C++20 · complete program · Download .cpp
#include <cassert>
#include <concepts>
template<int Length, int Time>
struct Quantity {
double value;
explicit constexpr Quantity(double v) : value(v) {}
};
template<int L, int T>
constexpr Quantity<L, T> operator+(Quantity<L, T> a, Quantity<L, T> b) {
return Quantity<L, T>{a.value + b.value};
}
template<int L1, int T1, int L2, int T2>
constexpr Quantity<L1 + L2, T1 + T2> operator*(
Quantity<L1, T1> a, Quantity<L2, T2> b) {
return Quantity<L1 + L2, T1 + T2>{a.value * b.value};
}
template<class A, class B>
concept Addable = requires(A a, B b) { a + b; };
using Length = Quantity<1, 0>;
using Duration = Quantity<0, 1>;
using Speed = Quantity<1, -1>;
static_assert(Addable<Length, Length>);
static_assert(!Addable<Length, Duration>);
static_assert(std::same_as<decltype(Speed{3} * Duration{4}), Length>);
int main() {
const Speed speed{3};
const Duration elapsed{4};
const Length traveled = speed * elapsed;
const auto total = traveled + Length{5};
assert(traveled.value == 12);
assert(total.value == 17);
}
Compile locally
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread books-cpp-template-metaprogramming.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
Without introducing division, represent acceleration and verify that “acceleration times time yields speed.”
Show a reference answer
Define using Acceleration = Quantity<1,-2>;. The type of the expression Acceleration{2} * Duration{3} is Quantity<1,-1>, that is Speed, with value 6. Add the corresponding std::same_as static assertion, then store the result as Speed and assert value == 6. This verifies both type composition and numeric computation.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.