-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
237: The <ratio> Header for Compile-Time Rational Arithmetic
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_Frequencyrepresenting 16 MHz (16,000,000/1). - Define a ratio type named
CycleDurationwhich is the reciprocal of the frequency (representing the length of a single clock cycle in seconds). - Use
std::ratio_multiplyto create a new type calledWait100Cyclesthat represents the time taken for 100 clock cycles. - In your
mainfunction, print the numerator and denominator ofWait100Cyclesto verify it has been simplified correctly by the compiler.
Hint: Remember that 100 cycles can be represented as std::ratio<100, 1>.
There are no comments for now.