Hunter Liu's Website

4. Week 2 Tuesday: Trouble with Doubles

≪ 3. Week 1 Thursday: Integer Operations | Table of Contents

Recall that when dividing two integers, C++ will always truncate the answer and produce another integer. We saw that this has its applications last week, but there are definitely scenarios in which you would prefer to just work with the decimal values instead. These are double types rather than int types.

Remark 1. What the heck is a double?

This is not necessary knowledge for this class, but I think it’s helpful to understand how computers store decimal numbers under the hood.

A single variable in C++ takes up a finite amount of storage on your computer; hence integers cannot store numbers that are too big in any direction (~10 digits). Yet doubles have an impressive range of numbers they can capture. How do they do this?

One should think of a double as a version of scientific notation. Consider the number \[6.0047382\times 10 ^{411},\] which is far too large for an integer to remember (you would need 411 digits, most of which are zeroes). Instead, a more efficient storage solution is to remember 6.0047382, and remember the exponent 411. This is what your computer does (in binary, more or less), and it explains why decimals have limited precion! Numbers with infinite decimal expansions will inevitably have to be cut off at some point in their scientific notation.

While one can always force a literal number to be a double by adding a .0 to the end of it, one has no such luxury with integer variables. Instead, one needs to “cast” integers into doubles. To do so, if a is an integer variable, then static_cast<double>(a) will turn a into a double.

1int a = 19; 
2int b = 3; 
3
4cout << a / b << endl;  // does integer division
5
6cout << static_cast<double>(a / b) << endl; // ??? 
7cout << static_cast<double>(a) / b << endl; 

Note the output of the second line! You need to cast to a double first, then perform the division. The second line prints out 6.0 because it does the division first (which produces the integer 6), then converts that into a double.

The problem of converting a double back into an integer is a similarly slippery problem, one that is perhaps best demonstrated with an example.

Example 2.

Write a C++ program that converts a duration in seconds into minutes, seconds, and milliseconds (you may assume this will be an integer). The program should prompt the user for an the duration in seconds.

SAMPLE RUNS: 
Enter a time in seconds: 124.556
124.556s = 2min, 4s, and 556ms. 

Let’s first write some pseudocode for this:

  1. Take a decimal input time from the user.
  2. Define the integer ms = time * 1000.
  3. Define the integer seconds = ms / 1000 and replace ms with ms % 1000.
  4. Define the integer minutes = seconds / 60 and replace seconds with seconds % 60.
  5. Print out the results to the screen.

We might naïvely cobble together the following program:

 1#include <iostream> 
 2
 3using namespace std; 
 4
 5int main() {
 6    // 1. take input from user 
 7    double time; 
 8    cout << "Enter a time in seconds: "; 
 9    cin >> time; 
10
11    // 2-4. compute ms, seconds, minutes 
12    int ms = time * 1000; 
13
14    int seconds = ms / 1000; 
15    ms = ms % 1000; 
16
17    int minutes = seconds / 60; 
18    seconds = seconds % 60; 
19
20    // 5. print everything out. 
21    cout << time << "s = " 
22         << minutes << "min, " 
23         << seconds << "s, and "
24         << ms << "ms." << endl; 
25
26    return 0; 
27} 

However, consider the following run of the code:

Enter a time in seconds: 16.002
16.002s = 0min, 16s, and 1ms.

Something went wrong, and it’s that C++ thinks 16.002 * 1000 is just barely less than 16002 due to the limited precision of a double! Thus it rounds it down to 16001 instead.

Common Mistake 3. Converting Doubles to Integers

There are several ways to convert doubles into integers. Consider the following code:

 1#include <cmath> // needed for floor, round, ceil 
 2
 3int main() {
 4    double d = 20.15; 
 5    int i1 = d * 100;        // truncates d * 100
 6    int i2 = floor(d * 100); 
 7    int i3 = round(d * 100); 
 8    int i4 = ceil(d * 100); 
 9
10    return 0; 
11} 

What values do you think these integer variables hold? It turns out that i1 and i2 will hold the value 2014 while the other two hold i3 and i4. What would happen if we defined d = -20.15 instead? Then i1, i2, and i3 hold the value -2015 while i4 holds -2014. Be very careful about which method you use to convert doubles to integers: they each behave in distinguishable ways, and they can cause many headaches.

With that out of the way, try to fix the code we wrote above yourself before peeking at the best way to proceed!

Solution
 1#include <iostream> 
 2#include <cmath>    // needed for round 
 3
 4using namespace std; 
 5
 6int main() {
 7    // 1. take input from user 
 8    double time; 
 9    cout << "Enter a time in seconds: "; 
10    cin >> time; 
11
12    // 2-4. compute ms, seconds, minutes 
13    int ms = round(time * 1000); 
14
15    int seconds = ms / 1000; 
16    ms = ms % 1000; 
17
18    int minutes = seconds / 60; 
19    seconds = seconds % 60; 
20
21    // 5. print everything out. 
22    cout << time << "s = " 
23         << minutes << "min, " 
24         << seconds << "s, and "
25         << ms << "ms." << endl; 
26
27    return 0; 
28} 

In general, it’s a good idea to use the round function from the cmath library to convert from doubles to integers over using the default integer conversion in C++.

Remark 4. The cmath library

In addition to having helpful functions like floor, ceil, and round, the cmath library defines a plethora of helpful mathematical operations that we may not know how to do otherwise. The C++ reference contains a list of every single function accessible through this library, some of which (e.g. fmin and fmax) may be immediately helpful for the first homework.

While the reference can be intimidating, it’s important that you get comfortable reading the documentation! There’s often no good reason to reivent the wheel.