Skip to content

6. Greatest Common Divisor

This time our goal is it to create a new project which allows us to do computations with rational numbers. C++ provides no built-in type able to store the value of a rational number. Instead C++ provides floating-point values which are able to approximate the value of rational numbers. For example, the value \(\frac{1}{3}\) cannot be represented by a floating-point value exactly.

In this lecture, we will not be fast enough to actually implement a new structure for rational numbers. Instead we think about an algorithm to compute the greatest common divisor (GCD). The GCD is needed to shorten a rational number after operations like addition or multiplication.

As before, the basic project structure will consist of three files, namely main.cpp, rational.hpp, and rational.cpp. So make sure to set it up properly.

$ pwd
/home/lyrahgames/projects/cpp-course
$ ls
01-hello  02-input  03-fibonacci  04-files
$ mkdir 05-rational
$ ls
01-hello  02-input  03-fibonacci  04-files  05-rational
$ cd 05-rational
$ pwd
/home/lyrahgames/projects/cpp-course/05-rational
$ touch main.cpp
$ touch rational.hpp
$ touch rational.cpp
$ ls
main.cpp  rational.hpp  rational.cpp

Euclid's Algorithm

Mathematically spoken, the function of the GCD, which we are calling \(\mathrm{gcd}\), can be defined by a recursive relation as follows.

\[ \mathrm{gcd}:\mathbb{N}\times\mathbb{N}\to\mathbb{N} \ ,\qquad \mathrm{gcd}(x,y) = \begin{cases} x &: y = 0 \\ \mathrm{gcd}(y,x\ \mathrm{mod}\ y) &: y\neq 0 \end{cases} \]

One of the most popular algorithms to compute the GCD was proposed more than two thousand years ago by Euclid. Actually, Euclid has constructed two different algorithms based on modulo and subtraction respectively. We will restrict our implementation to the modulo-based method. As always, first, we start with the declaration of our own implementation. We want to take two natural numbers and output the GCD which is also a natural number.

// rational.hpp
unsigned int gcd(unsigned int x, unsigned int y);

After copying this line into the rational.cpp source file, we have to add the actual implementation. Here, we do not want to talk about the mathematical details. I recommend to read through the according Wikipedia article.

// rational.cpp
unsigned int gcd(unsigned int x, unsigned int y){
  while(y != 0){
    auto tmp = y;
    y = x % y;
    x = tmp;
  }
  return x;
}

Afterwards, let us use the newly created function inside the main.cpp source file.

// main.cpp
#include <iostream>
#include <rational.hpp>

using namespace std;

int main(){
  for (int i = 1; i < 20; ++i)
    for (int j = 1; j < 20; ++j)
        cout << "gcd(" << i << ", " << j << ") = " << gcd(i,j) << '\n';
}

The project can be compiled by the following command.

g++ -o rational main.cpp rational.cpp -I.

Include Guard

In larger projects, a C++ header file will typically be included in different source files to provide necessary function declarations and other structures for in- and external use. Assume that in our main.cpp we would like to include the files A.hpp and B.hpp. Furthermore, let B.hpp include the file A.hpp. As a result from compiling the main.cpp file, the preprocessor will first insert the contents of the files A.hpp and B.hpp into the main.cpp file. At this point another #include <A.hpp> directive is left from file B.hpp. Therefore the preprocessor has to run one more time and will include the file A.hpp a second time into the main.cpp file. According to the one-definition rule (ODR), this may lead to errors in the compilation process because of multiple occurrences of structures that should only appear once. To prevent this kind of behavior, we will use a #pragma once directive at the start of every header file.

// rational.hpp
#pragma once

unsigned int gcd(unsigned int x, unsigned int y);

Note

The #pragma once directive for the preprocessor is not a standard construct from the C++ language. But it is provided by all the typical C++ compilers we will use in this lecture.

Inline

Sometimes a function may be too small in its implementation to feasibly separate it into a declaration and definition. To lighten up the development process, we would like to provide function definitions inside a header file, like in the following example.

// rational.hpp
unsigned int gcd(unsigned int x, unsigned int y){
  while(y != 0){
    auto tmp = y;
    y = x % y;
    x = tmp;
  }
  return x;
}

But due to the one-definition rule (ODR), if at least two source files include the given rational.hpp header file, an error has to occur because the function gcd would be defined in more than one source file. This is a difference to the above discussion about the #pragma once directive. This time we are talking about that a definition is only allowed to occur one single time in the whole project concerning all source and header files. We can loosen up this strict rule by using the C++ keyword inline on our function.

// rational.hpp
inline unsigned int gcd(unsigned int x, unsigned int y){
  while(y != 0){
    auto tmp = y;
    y = x % y;
    x = tmp;
  }
  return x;
}

The C++ specifier inline allows us to define the same function, in other words the interface or implementation must not change, in multiple source files but in each file the definition is only allowed to occur once. Practically spoken, with inline we can define a function inside a header file regardless of how many times we are including the file in different source files.

Exercises

  • Install the build2 build system toolchain according to the instructions.
  • Read the sections 1.1 and 1.3 in the Pro Git book to understand the purpose of a version control system (VCS).

References


Last update: October 22, 2020