Skip to Content
Course content

237: The <ratio> Header for Compile-Time Rational Arithmetic

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first encounter std::ratio, it's incredibly easy to mistake it for a general-purpose fraction class. I've seen plenty of developers try to use it to handle user input—like taking two integers from a console and trying to create a std::ratio to perform some precise math. They expect it to behave like a struct Fraction { int num, den; }; that you can manipulate while the program is running.

It's not a runtime Fraction class; it's a compile-time type

Here is why that line of thinking fails: std::ratio is a template, not a class you instantiate with variables. If you try to do something like this, your compiler will scream at you:

int n = 1;
int d = 3;
std::ratio<n, d> my_fraction; // ERROR: Template arguments must be constant expressions

The key insight is that std::ratio doesn't "store" values in the traditional sense. Instead, the values are baked into the type itself. When you declare std::ratio<1, 3>, you aren't creating an object that happens to hold 1 and 3; you are defining a unique type that represents the mathematical concept of one-third. I like to think of it as a way to perform arithmetic on the "blueprint" of your program before the program even starts executing.

Using ratio types to automate unit conversions

You might be wondering why on earth we'd want to move fractions into the type system. The real magic happens when you start combining these ratios to handle unit conversions without any runtime overhead. The most common place you'll see this is in std::chrono, but you can use it for anything involving fixed proportions.

Let's say we're working on a system that handles a specific hardware clock that ticks 1,000,000 times per second. We can define that relationship explicitly:

#include <iostream>
#include <ratio>

// Define a custom ratio for our hardware clock (1MHz)
using HardwareClock = std::ratio<1000000, 1>;

int main() {
    // Accessing the values via ::num and ::den
    std::cout << "Ticks per second: " << HardwareClock::num << "\n";
    
    // We can use ratio_divide to find how many seconds one tick is
    using OneTick = std::ratio_divide<std::ratio<1, 1>, HardwareClock>;
    
    std::cout << "One tick is " << OneTick::num << "/" << OneTick::den << " seconds.\n";
    return 0;
}

Notice that std::ratio automatically simplifies fractions for you. If you define std::ratio<10, 20>, the compiler internally reduces it to 1/2. This isn't happening via a function call at runtime; the compiler is doing the GCD (Greatest Common Divisor) math during the compilation phase.

Performing "Type-Math" with ratio aliases

Because these are types, you can't use + or * operators. Instead, you use the helper templates provided by the header: std::ratio_add, std::ratio_subtract, std::ratio_multiply, and std::ratio_divide. These take two ratio types as arguments and return a new ratio type.

I'll be honest: the syntax feels a bit clunky at first. But the payoff is massive. By the time your code is converted to machine instructions, all this ratio logic has vanished, replaced by the final, simplified constant. You get the readability of named units with the performance of hard-coded numbers.




📋 Practical Task

Building a Compile-Time Clock-Cycle to Seconds Converter

You are writing a driver for a legacy microcontroller. The CPU runs at a fixed frequency of 16 MHz (16,000,000 cycles per second). You need to create a system that calculates the duration of a specific number of clock cycles at compile time.

Your Task:

  • Define a ratio type named CPU_Frequency representing 16 MHz (16,000,000/1).
  • Define a ratio type named CycleDuration which is the reciprocal of the frequency (representing the length of a single clock cycle in seconds).
  • Use std::ratio_multiply to create a new type called Wait100Cycles that represents the time taken for 100 clock cycles.
  • In your main function, print the numerator and denominator of Wait100Cycles to verify it has been simplified correctly by the compiler.

Hint: Remember that 100 cycles can be represented as std::ratio<100, 1>.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.