We saw in the simple sum()
example that we can easily check a function is working by comparing
the return value with the expected correct value using the equivalence operator ==
. However,
this only works reliably for integer and boolean data types. When using floating-point data types
(float
and double
) the ==
operator may not work as expected due to
the way floating-point numbers are stored in memory. We may have two double
's holding the value
1.0
, but in fact one may actually be 1.000000000003232
and the other may be
1.000000000012565
. There is no saying that the ==
operator will return
true
and hence the test may fail.
It is surprisingly difficult to compare floating-point numbers and the C++ language has no simple, native way of doing so. Hence we need to write our own function.
The following is a home-brew function that works in most cases. Note that is relies on the C++ maths library
cmath
1 2 3 4 5 6 |
// checks whether 2 double's are almost equal bool almost_equal(double a, double b) { // == may return true, but also check whether the relative difference // between the numbers is less than a small threshold return a == b || (std::abs(a - b) / std::max(a, b) <= 1e-6); } |
The return value is made up of the logical OR of two operations. One operation uses a == b
as
the ==
does work in some cases. If that returns true
then the return value of the
function will be true
due to the logical OR. If that does not work and returns
false
then the second operation may return true
if the numbers are close enough
together.
std::abs(a - b) / std::max(a, b) <= 1e-6
This operation gets the absolute difference between the numbers and divides it by the biggest of the two
numbers. This gives us an idea of their relative difference. If that difference is less that a threshold
value, then the operation will return true
and we will consider the numbers to be essentially
equal.
This example is very similar to the sum()
one but instead considers a function
sqr()
that returns the square of a floating-point (double
) value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include <cmath> // for std::abs #include <iostream> #include <limits> // function we have written double sqr(double a); // function to test function bool test_sqr(double a, double expected); // function to run the tests int run_sqr_tests(); // tests whether two double are almost equal bool almost_equal(double a, double b); int main() { // test the function #ifdef DEBUG run_sqr_tests(); #endif } // function we have implemented double sqr(double a) { return a * a; } // function to test sqr bool test_sqr(double a, double expected) { std::cout.precision(std::numeric_limits<double>::max_digits10); std::cout << "sqr(" << a << ") : "; double val = sqr(a); // calc value and compare to expected if (almost_equal(val, expected)) { std::cout << "passed\n"; return true; } else { std::cout << "FAILED! " << val << " (expecting " << expected << ").\n"; return false; } } // function to run the tests int run_sqr_tests() { std::cout << "\nTesting sqr()...\n" << std::endl; // initialise counter for number of tests passed int passed = 0; // do various tests if (test_sqr(-1.123456789, 1.2621551568)) passed++; if (test_sqr(1.123456789e25, 1.2621552e+50)) passed++; if (test_sqr(1.123456789e-5, 1.2621552e-10)) passed++; if (test_sqr(3.0, 9)) passed++; if (test_sqr(7.678, 58.951684)) passed++; std::cout << "\nsqr() passed " << passed << " tests.\n"; return passed; } // checks whether 2 double's are almost equal bool almost_equal(double a, double b) { // == may return true, but also check whether the relative difference // between the numbers is less than a small threshold return a == b || (std::abs(a - b) / std::max(a, b) <= 1e-6); } |
Note that the test cases cover a HUGE range in numbers with answers ranging from 1050 to 10-10. That is 60 orders of magnitude and is pretty much unimaginable. If you imagine distance, 10-10 m is on the atomic scale while the diameter of the observable universe is thought to be on the order of 1026 m! Hence the difficulty in comparing numbers that can vary over such a huge range.
Running the code outputs the following. Note that the test_sqr()
function sets the precision of
cout
to be the maximum so we can see more decimal places.
Testing sqr()... sqr(-1.123456789) : passed sqr(1.123456789e+25) : passed sqr(1.123456789e-05) : passed sqr(3) : passed sqr(7.6779999999999999) : passed sqr() passed 5 tests.
We will now quickly look at what happens when the threshold in the almost_equal()
function is
reduced to 10-9. The following output is obtained.
Testing sqr()... sqr(-1.123456789) : passed sqr(1.123456789e+25) : FAILED! 1.2621551567501904e+50 (expecting 1.2621551999999999e+50). sqr(1.123456789e-05) : FAILED! 1.2621551567501907e-10 (expecting 1.2621552e-10). sqr(3) : passed sqr(7.6779999999999999) : passed sqr() passed 3 tests.
We can see that two tests now fail. These tests are for the extremely small and extremely large numbers. This can be caused by the limited accuracy of our expected answers that we provide to the testing function.
It is also worth pointing out that the difference between two big numbers can actually be very big, when they look to be very close. Consider the numbers 1.2345 x 106 and 1.23456 x 10 6. They look to be very close, but they are actually different by 500. This is the reason we use the relative difference between the double values. Think about currency. £500 is a 'big' discount when buying a car that costs £10000, but not when buying a mansion that costs £1.2 million!