Skip to content

7. Rational Structure

Before we start to actually implement a new type representing rational numbers, we will take a look at things that are possible by using built-in types, like int for integral number. The following code snippet demonstrates the declaration of two integer variables, the computation of their multiplication, and the output to the terminal.

int x = 2;
int y = 3;
auto z = x * y;
cout << "x = " << x << '\n'
     << "y = " << y << '\n'
     << "z = " << z << '\n';

Please remember, the keyword auto in the third line automatically deduces the type of the variable z by setting its type to the return type of the expression x * y. The code itself is straightforward and directly expresses our intention. We would like to have a similar code for our rational numbers. But first, let us take a look at a simpler approach. For this, we have to understand how rational numbers can be represented in a computer program.

Rational Numbers

In mathematics, a rational number can always be represented by a fraction. And vice versa, every fraction is a rational number. Hence, we can put it the following way.

Let \(r\in\mathbb{Q}\) be a rational number. Then there exists numbers \(p\in\mathbb{Z}\) and \(q\in\mathbb{N}\) such that \(r\) can be represented in the following way.

\[ r = \frac{p}{q} \]

We call \(p\) the numerator of \(r\) and \(q\) the denominator of \(r\). Let \(s\in\mathbb{Q}\) be a second rational number represented by the numbers \(m\in\mathbb{Z}\) and \(n\in\mathbb{N}\) acting as its numerator and denominator, respectively.

\[ s = \frac{m}{n} \]

In this case, the multiplication for the rational numbers \(r\) and \(s\) can calculated by using the following equation.

\[ r\cdot s = \frac{p}{q} \cdot \frac{m}{n} = \frac{p\cdot m}{q\cdot n} \]

Assume we would like to compute the multiplication of \(\frac{2}{3}\) and \(\frac{3}{7}\).

\[ \frac{2}{3} \cdot \frac{3}{7} = \frac{2\cdot 3}{3\cdot 7} = \frac{6}{21} \]

Simple Multiplication of Rationals

According to the mathematical theory, every rational number is represented by two integral numbers. As a consequence, the first step adjusting the above example to use two rational numbers could look like the following.

// First Rational Number
int x_numerator = 2;
int x_denominator = 3;

// Second Rational Number
int y_numerator = 3;
int y_denominator = 7;

// Result of Multiplication
auto z_numerator = x_numerator * y_numerator;
auto z_denominator = x_denominator * y_denominator;

// Output
cout << "x = " << x_numerator << '/' << x_denominator << '\n'
     << "y = " << y_numerator << '/' << y_denominator << '\n'
     << "z = " << z_numerator << '/' << z_denominator << '\n';

For every rational number, two variables of type int were taken to act as numerator and denominator, respectively. The multiplication of those rational numbers can then be executed by using the above formula. At the end, the results will be printed to the screen. The output should look like the following.

x = 2/3
y = 3/7
z = 6/21

Actual Goal

The approach that we have taken to compute the product of rational numbers is very simple. But it does not scale very well. If we would be bound to use many rational numbers, we would have to remember to declare the numerator and denominator for each rational number manually. Furthermore, every operation done with rational numbers acts on both the numerator and denominator. To manually write code like this for many variables would be cumbersome and error-prone. For this reason, we will strive for simplification in the following sense.

rational x = {2,3};
rational y = {3,7};
auto z = x * y;
cout << "x = " << x << '\n'
     << "y = " << y << '\n'
     << "z = " << z << '\n';

Basic Structure, Initialization, and Members

C++ provides and easy alternative. By using the keyword struct, we are able to create a new type with a custom name consisting of one or more other types. Initializing a variable of such a type, stores a variable for each type inside the assembled type. They belong to the assembled type and can be accessed and transferred by using the variable of the assembled type. Therefore those variables are called member variables.

// Structure defining a new type with name 'rational'
struct rational {
  int numerator;   // Member variable of type 'int' with name 'numerator'
  int denominator; // Member variable of type 'int' with name 'denominator'
};

Now, rational describes a new type in C++ and it can be used in a similar way as a built-in type, like int, to declare a variable. Typically, structures cannot be initialized by using a equality sign. C++ uses curly braces { and } for a uniform initialization syntax of all objects. This means that built-in types, too, can be initialized by these curly braces.

rational r{};
cout << "r = " << r.numerator << "/" << r.denominator << '\n';

The second line in the above code snippet showed how to access the variables inside the rational structure by using the . operator. Because the variable r was initialized with empty braces, the values of its numerator and denominator could be arbitrary. To prevent such a behavior, we will use default values for them at the start.

struct rational{
  int numerator = 0;
  int denominator = 1;
};

To not use the default values, we can use a comma-separated list to initialize member variables in the order of their appearance in their structure's definition.

rational r{1,2};

In this case, r.numerator would contain the value 1 and r.denominator the value 2.

Multiplication Operation

rational operator*(rational x, rational y){
  rational r{x.numerator * y.numerator, x.denominator * y.denominator};
  return r;
}
rational operator*(rational x, rational y){
  return rational{x.numerator * y.numerator, x.denominator * y.denominator};
}
rational operator*(rational x, rational y){
  return {x.numerator * y.numerator, x.denominator * y.denominator};
}
rational x{3,4};
rational y{5,7};
auto z = x * y;

Output Operation

std::ostream& operator<<(std::ostream& os, rational r){
  return os << r.numerator << '/' << r.denominator;
}
cout << "r = " << r << '\n';

Shorten by Using Euclidean Algorithm

rational shorten(rational r){
  const auto divisor = gcd(r.numerator, r.denominator);
  return {r.numerator / divisor, r.denominator / divisor};
}
rational p{6,8};
rational q{14,49};
p = shorten(p);
q = shorten(q);

Where to put everything?

Exercises

  • Add the other basic operations with rational numbers.

Last update: October 22, 2020