Chapter 5 - Errors

Drill

Below are 25 code fragments. Each is meant to be inserted into this “scaffolding”:

#include "std_lib_facilities.h"

int main()
try {
    <<your code here>>
    keep_window_open();
    return 0;
}
catch (exception& e) {
    cerr << "error: " << e.what() << '\n';
    keep_window_open();
    return 1;
}
catch (...) {
    cerr << "Oops: unknown exception!\n";
    keep_window_open();
    return 2;
}

Each has zero or more errors. Your task is to find and remove all errors in each program. When you have removed those bugs, the resulting program will compile, run, and write “Success!” Even if you think you have spotted an error, you still need to enter the (original, unimproved) program fragment and test it; you may have guessed wrong about what the error is, or there may be more errors in a fragment than you spotted. Also, one purpose of this drill is to give you a feel for how your compiler reacts to different kinds of errors. Do not enter the scaffolding 25 times — that’s a job for cut and paste or some similar “mechanical” technique. Do not fix problems by simply deleting a statement; repair them by changing, adding, or deleting a few characters.

  1. Cout << "Success!\n";

After inserting this fragment into the scaffolding and compiling the result is a compile-time error with the following output:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:10:5: error: use of undeclared identifier 'Cout'; did you mean 'cout'?
    Cout << "Success!\n";
    ^~~~
    cout
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/iostream:54:33: note: 'cout' declared here
extern _LIBCPP_FUNC_VIS ostream cout;
                                ^
1 error generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

After fixing the fragment to cout << "Success!\n"; the output is:

Success!
Please enter a character to exit
e

Process finished with exit code 0
  1. cout << "Success!\n;

The second fragment results also in a compile-time error:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:18:13: warning: missing terminating '"' character [-Winvalid-pp-token]
    cout << "Success!\n;
            ^
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:18:13: error: expected expression
1 warning and 1 error generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

To fix this we add a " after \n.

  1. cout << "Success" << !\n"

Here the compile-time error is:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:27:27: error: expected expression
    cout << "Success" << !\n"
                          ^
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:27:29: warning: missing terminating '"' character [-Winvalid-pp-token]
    cout << "Success" << !\n"
                            ^
1 warning and 1 error generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

In this case a " and terminating ; is missing.

  1. cout << success << '\n';

Again a compile-time error with the output:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:35:13: error: use of undeclared identifier 'success'
    cout << success << '\n';
            ^
1 error generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2

Wrapping success into quotation marks (string) solves the issue.

  1. string res = 7; vector<int> v(10); v[5] = res; cout << "Success!\n";

This fragment results in a compile-time error, in this case a type error because the string res cannot be assigned to the sixth vector element of type int.

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:40:12: error: no viable conversion from 'int' to 'std::__1::string' (aka 'basic_string<char, char_traits<char>, allocator<char> >')
    string res = 7;
           ^     ~
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/string:793:5: note: candidate constructor not viable: no known conversion from 'int' to 'const std::__1::basic_string<char> &' for 1st argument
    basic_string(const basic_string& __str);
    ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/string:798:5: note: candidate constructor not viable: no known conversion from 'int' to 'std::__1::basic_string<char> &&' for 1st argument
    basic_string(basic_string&& __str)
    ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/string:811:5: note: candidate constructor template not viable: no known conversion from 'int' to 'const char *' for 1st argument
    basic_string(const _CharT* __s) {
    ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/string:861:5: note: candidate constructor not viable: no known conversion from 'int' to 'initializer_list<char>' for 1st argument
    basic_string(initializer_list<_CharT> __il);
    ^
1 error generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

To fix this fragment a type change of the first assignment is required: string res = 7 to int res = 7.

  1. vector<int> v(10); v(5) = 7; if (v(5)!=7) cout << "Success!\n";

Fragment 6 results in another compile-time error and has also a logic error which can be fixed after correcting the compile-time error. The element at index 5 is equal to 7.

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:51:5: error: type 'Vector<int>' does not provide a call operator
    v6(5) = 7;
    ^~
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:52:9: error: type 'Vector<int>' does not provide a call operator
    if (v6(5)!=7)
        ^~
2 errors generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

The index operator [] is required to fix these two errors.

  1. if (cond) cout << "Success!\n"; else cout << "Fail!\n";

Compile-time error with the result:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:60:9: error: use of undeclared identifier 'cond'; did you mean 'cend'?
    if (cond)
        ^~~~
        cend
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/iterator:1731:6: note: 'cend' declared here
auto cend(const _Cp& __c) -> decltype(_VSTD::end(__c))
     ^
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:60:9: error: reference to overloaded function could not be resolved; did you mean to call it?
    if (cond)
        ^~~~
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/iterator:1731:6: note: possible target for call
auto cend(const _Cp& __c) -> decltype(_VSTD::end(__c))
     ^
2 errors generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2
  1. bool c = false; if (c) cout << "Success!\n"; else cout << "Fail!\n";

Running this code fragment results in the output:

Fail!
Please enter a character to exit
e

Process finished with exit code 0

To print out "Success!" the bool c needs to be true.

  1. string s = "ape"; boo c = "fool"<s; if (c) cout << "Success!\n";

The compile-time error output here is:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:83:5: error: use of undeclared identifier 'boo'; did you mean 'bool'?
    boo c9 = "fool" < s;
    ^~~
    bool
1 error generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

As suggested by the compiler, changing boo to bool fixes the error.

  1. string s = "ape"; if (s=="fool") cout << "Success!\n";

This fragment has a logic error. To print "Success!\n" the equal operator == needs to be changed to not equal !=.

  1. string s = "ape"; if (s=="fool") cout < "Success!\n";

This fragment has a compile-time error and a logic error:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:104:14: warning: result of comparison against a string literal is unspecified (use strncmp instead) [-Wstring-compare]
        cout < "Success!\n";
             ^ ~~~~~~~~~~~~
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:104:14: error: invalid operands to binary expression ('std::__1::ostream' (aka 'basic_ostream<char>') and 'const char [10]')
        cout < "Success!\n";
        ~~~~ ^ ~~~~~~~~~~~~
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/system_error:306:1: note: candidate function not viable: no known conversion from 'std::__1::ostream' (aka 'basic_ostream<char>') to 'const std::__1::error_condition' for 1st argument
operator<(const error_condition& __x, const error_condition& __y) _NOEXCEPT
^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/system_error:383:1: note: candidate function not viable: no known conversion from 'std::__1::ostream' (aka 'basic_ostream<char>') to 'const std::__1::error_code' for 1st argument
operator<(const error_code& __x, const error_code& __y) _NOEXCEPT
^
...

To fix the logic error, we need to change the equal operator == to not equal != or compare two strings that are equal.

  1. string s = "ape"; if (s+"fool") cout < "Success!\n";

This fragment results in two compile-time errors:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:114:9: error: no viable conversion from 'std::__1::basic_string<char>' to 'bool'
    if (s12+"fool")
        ^~~~~~~~~~
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/string:869:5: note: candidate function
    operator __self_view() const _NOEXCEPT { return __self_view(data(), size()); }
    ^
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:115:14: warning: result of comparison against a string literal is unspecified (use strncmp instead) [-Wstring-compare]
        cout < "12. Success!\n";
             ^ ~~~~~~~~~~~~~~~~
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:115:14: error: invalid operands to binary expression ('std::__1::ostream' (aka 'basic_ostream<char>') and 'const char [14]')
        cout < "12. Success!\n";
        ~~~~ ^ ~~~~~~~~~~~~~~~~
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/system_error:306:1: note: candidate function not viable: no known conversion from 'std::__1::ostream' (aka 'basic_ostream<char>') to 'const std::__1::error_condition' for 1st argument
operator<(const error_condition& __x, const error_condition& __y) _NOEXCEPT
^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/system_error:383:1: note: candidate function not viable: no known conversion from 'std::__1::ostream' (aka 'basic_ostream<char>') to 'const std::__1::error_code' for 1st argument
operator<(const error_code& __x, const error_code& __y) _NOEXCEPT
^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/utility:594:1: note: candidate template ignored: could not match 'pair' against 'basic_ostream'
operator< (const pair<_T1,_T2>& __x, const pair<_T1,_T2>& __y)
^
...

To fix the first compile-time error we have to replace + with != because comparison of a string literal in a condition of an if-statement is unspecified. The second error is that we use < (the less-than operator) rather than the << (the output operator).

  1. vector<char> v(5); for (int i=0; 0<v.size(); ++i) ; cout << "Success!\n";

Output of this fragment is Success! but there are two logic errors:

  • The semicolon after the condition and control variable i of the for statement ends this loop statement and executes the following statement cout << "Success!\n";. To fix this the semecolon needs to be removed.
  • The logical comparison of 0<v.size() is always true if vector v contains elements. Here the solution is to use the iterator variable i instead of 0.

After fixing these logic errors, the output is five times Success!:

Success!
Success!
Success!
Success!
Success!
  1. vector<char> v(5); for (int i=0; i<=v.size(); ++i) ; cout << "Success!\n";

Similar to the previous fragment 13 with one logic error. The semicolon after the condition and control variable i of the for-statement needs to be removed in order to get the following output:

Success!
Success!
Success!
Success!
Success!
Success!
  1. string s = "Success!\n"; for (int i=0; i<6; ++i) cout << s[i];

Running this program we don’t get the complete Success!\n string. Instead:

SuccesPlease enter a character to exit

This logic error is fixed when using the v.size() instead of the magic number 6 in the condition of the for-statement.

  1. if (true) then cout << "Success!\n"; else cout << "Fail!\n";

This results in two compile-time errors:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:152:15: error: unknown type name 'then'
    if (true) then
              ^
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:153:13: error: expected ';' at end of declaration
        cout << "16. Success!\n";
            ^
            ;
2 errors generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

The compiler assumes that then is a type and cout a variable name, which is why the compiler expects a ; after cout. To fix this fragment, we only have to remove then which is sometimes used in other languages.

  1. int x = 2000; char c = x; if (c==2000) cout << "Success!\n";

This fragment compiles and runs but gives no output because of a narrowing error. The conversion from an int that is too large to fit into a char (2000 in this case) leads to a different char value and therefore false in the condition of the if-statement, when comparing the literal 2000 to the char c. To fix this error c needs to be of type int instead of char.

  1. string s = "Success!\n"; for (int i=0; i<10; ++i) cout << s[i];

Executing this fragment can lead to a runtime error or in a strange output becaues with the magic number 10 in the conditon check of the for-statement we output too many characters of the string Success!\n which has 9 characters.

To fix this fragment, the size() operator of string should be used.

  1. vector v(5); for (int i=0; i<=v.size(); ++i) ; cout << "Success!\n";

Trying to compile this fragment results in the following compile-time error:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:184:12: error: no viable constructor or deduction guide for deduction of template arguments of 'Vector'
    vector v(5);
           ^
/Users/fjp/git/ppp/PPP2code/std_lib_facilities.h:70:27: note: candidate template ignored: could not match 'Vector<T>' against 'int'
template< class T> struct Vector : public std::vector<T> {
                          ^
/Users/fjp/git/ppp/PPP2code/std_lib_facilities.h:70:27: note: candidate function template not viable: requires 0 arguments, but 1 was provided
1 error generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

vector requires a template argument, which describes its underlying type used. In this case int is missing: vector<int>. The fragment contains also a logic error. The semicolon after the control variable and the condition of the for-statement needs to be removed.

After fixing those errors the output is:

Success!
Success!
Success!
Success!
Success!
Success!
  1. int i=0; int j = 9; while (i<10) ++j; if (j<i) cout << "Success!\n";

The fragment contains an endless loop because of a logic error. Instead of j being incremented inside the block of the while-statement, i, the control variable should be incremented. Fixing this logic error results in the desired output Success!.

  1. int x = 2; double d = 5/(x–2); if (d==2*x+0.5) cout << "Success!\n";

This fragment contains multiple errors. Because int x = 2 we would divide by zero in the next statement: double d = 5/(x-2). However, on mac osx d results in inf. To fix this we have to use either a different value for x or use another equation for d. This is a quadratic equation that has the solutions: x1 = 7/8 + sqrt(241)/8 and x2 = 7/8 - sqrt(241)/8 (https://www.wolframalpha.com/input/?i=4*x%5E2+-+7*x+-+12+%3D+0). However, to get the equality check condition of the if-statement evaluate to true, we would have to use epsilon (small value) and work with something like abs(d - 2*x+0.5) < eps. Because abs was not introduced, I changed the line double d = 5/(x-2) to double d = 5.0/x + 2.0. Using floating-point precision in this equation also solves the narrowing error which was another error.

  1. string<char> s = "Success!\n"; for (int i=0; i<=10; ++i) cout << s[i];

Compile-time error output of this fragment is:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:227:11: error: expected unqualified-id
    string<char> s = "Success!\n"; for (int i=0; i<=10; ++i) cout << s[i];
          ^
1 error generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

After fixing the compile-time error by removing the wrong template argument <char> the fragment outputs to many characters of the string Success!\n which has length 9 instead of 10. To fix this we should use the size() of the string instead of the magic number 9 inside the condition of the for-statement. Another problem is the less than equal <= check in the condition of the for loop. This needs to be a check using less than < because C++ uses zero based indices.

  1. int i=0; while (i<10) ++j; if (j<i) cout << "Success!\n";

Output of this fragment results in two compile-time errors:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:244:11: error: use of undeclared identifier 'j'; did you mean 'i'?
        ++j;
          ^~~
          i
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:242:9: note: 'i' declared here
    int i = 0;
        ^
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:242:9: error: use of undeclared identifier 'j'; did you mean 'i'?
    if (j<i)
        ^~~
        i
/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:239:9: note: 'i' declared here
    int i = 0;
        ^
2 errors generated.
make[3]: *** [CMakeFiles/Ch5Drill.dir/scaffolding.cpp.o] Error 1
make[2]: *** [CMakeFiles/Ch5Drill.dir/all] Error 2
make[1]: *** [CMakeFiles/Ch5Drill.dir/rule] Error 2
make: *** [Ch5Drill] Error 2

After defining j the result is an endless loop because of a logic error inside the block of the while-statement. Fixing this logic error by incrementing i instead of j the output is: Sucess!\n.

  1. int x = 4; double d = 5/(x-2); if (d=2*x+0.5) cout << "Success!\n";

This fragment works at first try but only because the condition of the if-statement is an assignment, which is probably wrong. After changing this to an equality == check, it is the same as fragment 21.

  1. cin << "Success!\n";

This fragment ends in a compile-time error output and the compiler output of this is:

/Users/fjp/git/ppp/ch5-errors/drill/scaffolding.cpp:259:9: error: invalid operands to binary expression ('std::__1::istream' (aka 'basic_istream<char>') and 'const char [10]')
    cin << "Success!\n";
    ~~~ ^  ~~~~~~~~~~~~
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/type_traits:4830:3: note: candidate function template not viable: no known conversion from 'std::__1::istream' (aka 'basic_istream<char>') to 'std::byte' for 1st argument
  operator<< (byte  __lhs, _Integer __shift) noexcept
  ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/ostream:748:1: note: candidate template ignored: could not match 'basic_ostream' against 'basic_istream'
operator<<(basic_ostream<_CharT, _Traits>& __os, _CharT __c)
^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/ostream:755:1: note: candidate template ignored: could not match 'basic_ostream' against 'basic_istream'
operator<<(basic_ostream<_CharT, _Traits>& __os, char __cn)
^
...

The compiler thinks we try to compare cin against string "Success!\n" using the less than operator < and finds that the operands are invalid. To fix this logic error, we have to use cout instead of cin.

Review

  1. Name four major types of errors and briefly define each one.
  • Compile-time errors: Errors found by the compiler. We can further classify compile-time errors based on which language rules they violate, for example:
    • Syntax errors
    • Type errors
  • Link-time errors: Errors found by the linker when it is trying to combine object files into an executable program.
  • Run-time errors: Errors found by checks in a running program. We can further classify run-time errors as
    • Errors detected by the computer (hardware and/or operating system)
    • Errors detected by a library (e.g., the standard library)
    • Errors detected by user code
  • Logic errors: Errors found by the programmer looking for the causes of erroneous results.
  1. What kinds of errors can we ignore in student programs?

We will assume that your program

1. Should produce the desired results for all legal inputs
2. Should give reasonable error messages for all illegal inputs
3. Need not worry about misbehaving hardware
4. Need not worry about misbehaving system software
5. Is allowed to terminate after finding an error

Essentially all programs for which assumptions 3, 4, or 5 do not hold can be considered advanced and beyond the scope of this book. However, assumptions 1 and 2 are included in the definition of basic professionalism.

  1. What guarantees should every completed project offer?

We will assume that your program

1. Should produce the desired results for all legal inputs
2. Should give reasonable error messages for all illegal inputs
3. Need not worry about misbehaving hardware
4. Need not worry about misbehaving system software
5. Is allowed to terminate after finding an error

Essentially all programs for which assumptions 3, 4, or 5 do not hold can be considered advanced and beyond the scope of this book. However, assumptions 1 and 2 are included in the definition of basic professionalism.

  1. List three approaches we can take to eliminate errors in programs and produce acceptable software.

Basically, we offer three approaches to producing acceptable software:

  • Organize software to minimize errors.
  • Eliminate most of the errors we made through debugging and testing.
  • Make sure the remaining errors are not serious. None of these approaches can completely eliminate errors by itself; we have to use all three.

Experience matters immensely when it comes to producing reliable programs, that is, programs that can be relied on to do what they are supposed to do with an acceptable error rate. Please don’t forget that the ideal is that our programs always do the right thing. We are usually able only to approximate that ideal, but that’s no excuse for not trying very hard.

Start thinking about debugging before you write the first line of code. Once you have a lot of code written it’s too late to try to simplify debugging. Decide how to report errors: “Use error() and catch exception in main()” will be your default answer in this book.

Make the program easy to read so that you have a chance of spotting the bugs:

  • Comment your code well. That doesn’t simply mean “Add a lot of comments.” You don’t say in English what is better said in code. Rather, you say in the comments — as clearly and briefly as you can — what can’t be said clearly in code:
    • The name of the program
    • The purpose of the program
    • Who wrote this code and when
    • Version numbers
    • What complicated code fragments are supposed to do
    • What the general design ideas are
    • How the source code is organized
    • What assumptions are made about inputs
    • What parts of the code are still missing and what cases are still not handled
  • Use meaningful names. That doesn’t simply mean “Use long names.”
  • Use a consistent layout of code. Your IDE tries to help, but it can’t do everything and you are the one responsible.
  • Break code into small functions, each expressing a logical action. Try to avoid functions longer than a page or two; most functions will be much shorter.
  • Avoid complicated code sequences. Try to avoid nested loops, nested if-statements, complicated conditions, etc. Unfortunately, you sometimes need those, but remember that complicated code is where bugs can most easily hide
  • Use library facilities rather than your own code when you can. A library is likely to be better thought out and better tested than what you could produce as an alternative while busily solving your main problem.
  1. Why do we hate debugging?

Debugging works roughly like this:

1. Get the program to compile.
2. Get the program to link.
3. Get the program to do what it is supposed to do.

Basically, we go through this sequence again and again: hundreds of times, thousands of times, again and again for years for really large programs. Each time something doesn’t work we have to find what caused the problem and fix it. I consider debugging the most tedious and timewasting aspect of programming and will go to great lengths during design and programming to minimize the amount of time spent hunting for bugs. Others find that hunt thrilling and the essence of programming — it can be as addictive as any video game and keep a programmer glued to the computer for days and nights (I can vouch for that from personal experience also).

  1. What is a syntax error? Give five examples.
int s1 = area(7; // error: ) missing
int s2 = area(7) // error: ; missing
Int s3 = area(7); // error: Int is not a type
int s4 = area('7); // error: non-terminated character (' missing)
int s5 = area(7): // error: semicolon missing

string x1 = "5; // error: non-terminated string (" missing)
vector<int> v(10); v(3) = 2; // error: wrong access operator () instead of []

Each of those lines has a syntax error; that is, they are not well formed according to the C++ grammar, so the compiler will reject them. Unfortunately, syntax errors are not always easy to report in a way that you, the programmer, find easy to understand. That’s because the compiler may have to read a bit further than the error to be sure that there really is an error. The effect of this is that even though syntax errors tend to be completely trivial (you’ll often find it hard to believe you have made such a mistake once you find it), the reporting is often cryptic and occasionally refers to a line further on in the program. So, for syntax errors, if you don’t see anything wrong with the line the compiler points to, also look at previous lines in the program.

  1. What is a type error? Give five examples.

Type errors are mismatches between the types you declared (or forgot to declare) for your variables, functions, etc. and the types of values or expressions you assign to them, pass as function arguments, etc. For example:

int x0 = arena(7); // error: undeclared function
int x1 = area(7); // error: wrong number of arguments in case area requires two arguments
int x2 = area("seven",2); // error: 1st argument has a wrong type
int x3 = area("seven","three"); // error: both arguments have a wrong type
int x4 = area(1,"three"); // error: 2nd argument has a wrong type
string x5 = area(7); // error: wrong return type if area is returning an int. There is no direct conversion from int to string
  1. What is a linker error? Give three examples.

A program consists of several separately compiled parts, called translation units. Every function in a program must be declared with exactly the same type in every translation unit in which it is used. We use header files to ensure that; see §8.3. Every function must also be defined exactly once in a program. If either of these rules is violated, the linker will give an error. Here is an example of a program that might give a typical linker error:

int area(int length, int width); // calculate area of a rectangle
int main()
{
int x = area(2,3);
}

Unless we somehow have defined area() in another source file and linked the code generated from that source file to this code, the linker will complain that it didn’t find a definition of area().

The definition of area() must have exactly the same types (both the return type and the argument types) as we used in our file, that is:

int area(int x, int y) { /* . . . */ } // "our" area()

Functions with the same name but different types will not match and will be ignored:

double area(double x, double y) { /* . . . */ } // not "our" area()
int area(int x, int y, char unit) { /* . . . */ } // not "our" area()

Note that a misspelled function name doesn’t usually give a linker error. Instead, the compiler gives an error immediately when it sees a call to an undeclared function. Compile-time errors are found earlier than link-time errors and are typically easier to fix. The linkage rules for functions, as stated above, also hold for all other entities of a program, such as variables and types: there has to be exactly one definition of an entity with a given name, but there can be many declarations, and all have to agree exactly on its type. For more details, see §8.2–3.

  1. What is a logic error? Give three examples.

Once we have removed the initial compiler and linker errors, the program runs. Typically, what happens next is that no output is produced or that the output that the program produces is just wrong. This can occur for a number of reasons. Maybe your understanding of the underlying program logic is flawed; maybe you didn’t write what you thought you wrote; or maybe you made some “silly error” in one of your if-statements, or whatever. Logic errors are usually the most difficult to find and eliminate, because at this stage the computer does what you asked it to.

The following program would output nothing because variable a is assigned zero again in the parentheses of the if-statement instead of using the equal operator ==.

int a = 0;
if (a = 0)
    cout << "a is equal to zero\n";

Another mistake can happen when < and > are mistakenly swapped. In the following example, the block of the while-loop would never be entered.

// initialize a
int a = 0;
while (a > 10)
{
    // ... do something
    ++a;
}

The following function tries to find the minimum integer in a vector and return it:

int findMinimum(vector<int> v)
{
    int minimum = 0;
    for (int element : v)
    {
        if (element < minimum)
            minimum = element;
    }
    return minimum;
}

Calling this function with a vector that contains only positive numbers results in a wrong return value. At least logically according to what the function was intended to do.

int main()
{
    vector<int> v = {1, 5, 6, 1};
    cout << "Minimum of v is: " << findMinimum(v) << '\n';
}
  1. List four potential sources of program errors discussed in the text.

Here are some sources of errors:

  • Poor specification: If we are not specific about what a program should do, we are unlikely to adequately examine the “dark corners” and make sure that all cases are handled (i.e., that every input gives a correct answer or an adequate error message).
  • Incomplete programs: During development, there are obviously cases that we haven’t yet taken care of. That’s unavoidable. What we must aim for is to know when we have handled all cases.
  • Unexpected arguments: Functions take arguments. If a function is given an argument we don’t handle, we have a problem. An example is calling the standard library square root function with -1.2: sqrt(-1.2). Since sqrt() of a double returns a double, there is no possible correct return value. §5.5.3 discusses this kind of problem.
  • Unexpected input: Programs typically read data (from a keyboard, from files, from GUIs, from network connections, etc.). A program makes many assumptions about such input, for example, that the user will input a number. What if the user inputs ‘aw, shut up!’ rather than the expected integer? §5.6.3 and §10.6 discuss this kind of problem.
  • Unexpected state: Most programs keep a lot of data (“state”) around for use by different parts of the system. Examples are address lists, phone directories, and vectors of temperature readings. What if such data is incomplete or wrong? The various parts of the program must still manage. §26.3.5 discusses this kind of problem.
  • Logical errors: That is, code that simply doesn’t do what it was supposed to do; we’ll just have to find and fix such problems. §6.6 and §6.9 give examples of finding such problems.

This list has a practical use. We can use it as a checklist when we are considering how far we have come with a program. No program is complete until we have considered all of these potential sources of errors. In fact, it is prudent to keep them in mind from the very start of a project, because it is most unlikely that a program that is just thrown together without thought about errors can have its errors found and removed without a serious rewrite.

  1. How do you know if a result is plausible? What techniques do you have to answer such questions?

The point is that unless we have some idea of what a correct answer will be like — even ever so approximately — we don’t have a clue whether our result is reasonable. Always ask yourself this question:

1. Is this answer to this particular problem plausible?

You should also ask the more general (and often far harder) question:

2. How would I recognize a plausible result?

Here, we are not asking, “What’s the exact answer?” or “What’s the correct answer?” That’s what we are writing the program to tell us. All we want is to know that the answer is not ridiculous. Only when we know that we have a plausible answer does it make sense to proceed with further work. Estimation is a noble art that combines common sense and some very simple arithmetic applied to a few facts. Some people are good at doing estimates in their heads, but we prefer scribbles “on the back of an envelope” because we find we get confused less often that way. What we call estimation here is an informal set of techniques that are sometimes (humorously) called guesstimation because they combine a bit of guessing with a bit of calculation.

  1. Compare and contrast having the caller of a function handle a run-time error vs. the called function’s handling the run-time error.

Checking arguments in the function seems so simple, so why don’t people do that always? Inattention to error handling is one answer, sloppiness is another, but there are also respectable reasons:

  • We can’t modify the function definition: The function is in a library that for some reason can’t be changed. Maybe it’s used by others who don’t share your notions of what constitutes good error handling. Maybe it’s owned by someone else and you don’t have the source code. Maybe it’s in a library where new versions come regularly so that if you made a change, you’d have to change it again for each new release of the library.
  • The called function doesn’t know what to do in case of error: This is typically the case for library functions. The library writer can detect the error, but only you know what is to be done when an error occurs.
  • The called function doesn’t know where it was called from: When you get an error message, it tells you that something is wrong, but not how the executing program got to that point. Sometimes, you want an error message to be more specific.
  • Performance: For a small function the cost of a check can be more than the cost of calculating the result. For example, that’s the case with area(), where the check also more than doubles the size of the function (that is, the number of machine instructions that need to be executed, not just the length of the source code). For some programs, that can be critical, especially if the same information is checked repeatedly as functions call each other, passing information along more or less unchanged. So what should you do? Check your arguments in a function unless you have a good reason not to.

We can have the called function do the detailed checking, while letting each caller handle the error as desired. This approach seems like it could work, but it has a couple of problems that make it unusable in many cases:

  • Now both the called function and all callers must test. The caller has only a simple test to do but must still write that test and decide what to do if it fails.
  • A caller can forget to test. That can lead to unpredictable behavior further along in the program.
  • Many functions do not have an “extra” return value that they can use to indicate an error. For example, a function that reads an integer from input (such as cin’s operator >>) can obviously return any int value, so there is no int that it could return to indicate failure. The second case above — a caller forgetting to test — can easily lead to surprises.
  1. Why is using exceptions a better idea than returning an “error value”?

The fundamental idea is to separate detection of an error (which should be done in a called function) from the handling of an error (which should be done in the calling function) while ensuring that a detected error cannot be ignored;

The basic idea is that if a function finds an error that it cannot handle, it does not return normally; instead, it throws an exception indicating what went wrong. Any direct or indirect caller can catch the exception, that is, specify what to do if the called code used throw. A function expresses interest in exceptions by using a try-block (as described in the following subsections) listing the kinds of exceptions it wants to handle in the catch parts of the try-block. If no caller catches an exception, the program terminates.

  1. How do you test if an input operation succeeded?

Once bad input is detected, it is dealt with using the same techniques and language features as argument errors and range errors. Here, we’ll just show how you can tell if your input operations succeeded. Consider reading a floatingpoint number:

double d = 0;
cin >> d;

We can test if the last input operation succeeded by testing cin:

if (cin) {
// all is well, and we can try reading again
}
else {
// the last read didn’t succeed, so we take some other action
}

There are several possible reasons for that input operation’s failure. The one that should concern you right now is that there wasn’t a double for >> to read.

  1. Describe the process of how exceptions are thrown and caught.

The basic idea is that if a function finds an error that it cannot handle, it does not return normally; instead, it throws an exception indicating what went wrong. Any direct or indirect caller can catch the exception, that is, specify what to do if the called code used throw. A function expresses interest in exceptions by using a try-block listing the kinds of exceptions it wants to handle in the catch parts of the try-block. If no caller catches an exception, the program terminates.

  1. Why, with a vector called v, is v[v.size()] a range error? What would be the result of calling this?

The size() method of a vector returns the number of elements in that vector. C++ uses zero-based numbering which means that the first index of a vector or array is zero. The last element is indexed using v.size()-1.

Accessing v[v.size()] results in a range error because we try to access memory that we aren’t allowed to read or write. It lies outside the allocated memory of the vector v.

  1. Define pre-condition and post-condition; give an example (that is not the area() function from this chapter), preferably a computation that requires a loop.

To deal with bad arguments to a function, the call of a function is basically the best point to think about correct code and to catch errors: this is where a logically separate computation starts (and ends on the return).

A requirement of a function upon its argument is often called a pre-condition: it must be true for the function to perform its action correctly.

The following example shows a function that uses a pre-condition to check if the argument is positive, which is documented after the function signature.

double positive_sqrt(double a)
// check that the argument is positive
{
    if (!(0<a)) // ! means "not"
        error("bad arguments for positive_sqrt");

    return sqrt(a);
}

This example checks for bad arguments and reports them by throwing the string bad arguments for positive_sqrt Another way to deal with bad arguments would be to ignore it and hope/assume that all callers give correct arguments.

With post-conditions we can check the return value, which is useful because we know the type that is returned from a function.

double rectangle_circumference(double height, double width)
// check that the arguments are positive
{
    if (!(0<height && 0<width)) // ! means "not" and && means "and"
        error("bad arguments for rectangle_circumference");

    double circumference = 2*height + 2*width;
    if (circumference <= 0) error("rectangle_circumference() post-condition");
    return circumference;
}
  1. When would you not test a pre-condition?

The reasons most often given for not checking pre-conditions are:

  • Nobody would give bad arguments.
  • It would slow down my code.
  • It is too complicated to check.

The first reason can be reasonable only when we happen to know “who” calls a function - and in real-world code that can be very hard to know.

The second reason is valid far less often than people think and should most often be ignored as an example of “premature optimization.” You can always remove checks if they really turn out to be a burden. You cannot easily gain the correctness they ensure or get back the nights’ sleep you lost looking for bugsthose tests could have caught.

The third reason is the serious one. It is easy (once you are an experienced programmer) to find examples where checking a pre-condition would take significantly more work than executing the function. An example is a lookup in a dictionary: a pre-condition is that the dictionary entries are sorted - and verifying that a dictionary is sorted can be far more expensive than a lookup.

  1. When would you not test a post-condition?

Similar to the previous answer, here are two reasons not to test post-conditions:

  • It would slow down my code.
  • It is too complicated to check.

For example:

int area(int length, int width)
// calculate area of a rectangle;
// pre-conditions: length and width are positive
// post-condition: returns a positive value that is the area
{
    if (length<=0 || width <=0) error(“area() pre-condition”);
    int a = length*width;
    if (a<=0) error(“area() post-condition”);
    return a;
}

We couldn’t check the complete post-condition, but we checked the part that said that it should be positive.

  1. What are the steps in debugging a program?

The activity of deliberately searching for errors and removing them is called debugging.

Debugging works roughly like this:

1. Get the program to compile.
2. Get the program to link.
3. Get the program to do what it is supposed to do.

Basically, we go through this sequence again and again: hundreds of times, thousands of times, again and again for years for really large programs. Each time something doesn’t work we have to find what caused the problem and fix it.

  1. Why does commenting help when debugging?

It makes the program easy to read so that you have a chance of spotting the bugs. Here are some advices for commenting:

  • Comment your code well. That doesn’t simply mean “Add a lot of comments.” You don’t say in English what is better said in code. Rather, you say in the comments - as clearly and briefly as you can - what can’t be said clearly in code:
  • The name of the program
  • The purpose of the program
  • Who wrote this code and when
  • Version numbers
  • What complicated code fragments are supposed to do
  • What the general design ideas are
  • How the source code is organized
  • What assumptions are made about inputs
  • What parts of the code are still missing and what cases are still not handled
  1. How does testing differ from debugging?

In addition to debugging we need a systematic way to search for errors. This is called testing. Basically, testing is executing a program with a large and systematically selected set of inputs and comparing the results to what was expected. A run with a given set of inputs is called a test case. Realistic programs can require millions of test cases. Basically, systematic testing cannot be done by humans typing in one test after another. Instead we use tools necessary to properly approach testing. Remember that we have to approach testing with the attitude that finding errors is good. Consider:

  • Attitude 1: I’m smarter than any program! I’ll break that @#$%^ code!
  • Attitude 2: I polished this code for two weeks. It’s perfect!

Who do you think will find more errors? Of course, the very best is an experienced person with a bit of “attitude 1” who coolly, calmly, patiently, and systematically works through the possible failings of the program. Good testers are worth their weight in gold.

We try to be systematic in choosing our test cases and always try both correct and incorrect inputs.

Terms

argument error

assertion

catch

compile-time error

container

debugging

error

exception

invariant

logic error

post-condition

pre-condition

range error

requirement

run-time error

syntax error

testing

throw

type error

Try This

Compiler Response

Try to compile those examples and see how the compiler responds.

The output of clang to this program is:

/Users/fjp/git/ppp/ch5-errors/trythis/01-compiler_response/main.cpp:7:20: error: expected ')'
    int s1 = area(7; // error: ) missing
                   ^
/Users/fjp/git/ppp/ch5-errors/trythis/01-compiler_response/main.cpp:7:18: note: to match this '('
    int s1 = area(7; // error: ) missing
                 ^
/Users/fjp/git/ppp/ch5-errors/trythis/01-compiler_response/main.cpp:8:14: error: no matching function for call to 'area'
    int s2 = area(7) // error: ; missing
             ^~~~
/Users/fjp/git/ppp/ch5-errors/trythis/01-compiler_response/main.cpp:3:5: note: candidate function not viable: requires 2 arguments, but 1 was provided
int area(int length, int width); // calculate area of a rectangle
    ^
/Users/fjp/git/ppp/ch5-errors/trythis/01-compiler_response/main.cpp:10:19: error: use of undeclared identifier 'ʻ7'
    int s4 = area(ʻ7); // error: non-terminated character ( missing)
                  ^
3 errors generated.

Compiler Response 2

Try to compile those examples and see how the compiler responds. Try thinking of a few more errors yourself, and try those.

The output of clang to this program is:

/Users/fjp/git/ppp/ch5-errors/trythis/02-compiler_response/main.cpp:7:14: error: use of undeclared identifier 'arena'
    int x0 = arena(7); // error: undeclared function
             ^
/Users/fjp/git/ppp/ch5-errors/trythis/02-compiler_response/main.cpp:8:14: error: no matching function for call to 'area'
    int x1 = area(7); // error: wrong number of arguments
             ^~~~
/Users/fjp/git/ppp/ch5-errors/trythis/02-compiler_response/main.cpp:3:5: note: candidate function not viable: requires 2 arguments, but 1 was provided
int area(int length, int width); // calculate area of a rectangle
    ^
/Users/fjp/git/ppp/ch5-errors/trythis/02-compiler_response/main.cpp:9:14: error: no matching function for call to 'area'
    int x2 = area("seven",2); // error: 1st argument has a wrong type
             ^~~~
/Users/fjp/git/ppp/ch5-errors/trythis/02-compiler_response/main.cpp:3:5: note: candidate function not viable: no known conversion from 'const char [6]' to 'int' for 1st argument
int area(int length, int width); // calculate area of a rectangle
    ^
3 errors generated.

Error Reporting

Test this program with a variety of values. Print out the values of area1, area2, area3, and ratio. Insert more tests until all errors are caught. How do you know that you caught all errors? This is not a trick question; in this particular example you can give a valid argument for having caught all errors.

To run the given example function f(int x, int y, int z) I added the required functions area(int x, int y) from section 5.5.3 and framed_area(int x, int y) from section 5.5.2 including the error() function from the std_lib_facilities.h header.

It is not possible to test this program with a variety of values because the first call to int area2 = framed_area(1,z) terminates the program with an error. This happens because the first input argument 1 yields a negative value when subtracted by the constexpr int frame_width = 2. The following program is an extension to the original errorreporting.cpp to fix those issues and add tests where appropriate. In this version framed_area() does not use error(). Instead the return value of area() is return directly which is -1 in case of an error.

Here is one output that is equal for both programs:

Enter three integers separated by space (followed by 'Enter')
1 1 1
libc++abi.dylib: terminating with uncaught exception of type std::runtime_error: non-positive area() argument called by framed_area()

Here is the output with values that are working:

Enter three integers separated by space (followed by 'Enter')
3 3 3
area1: 9
area3: 1
ratio: 9
area4: 1
area5: 5

Calling area with values that result in an area greater than the size of an integer (32 bit) will result in an unrecognized overflow error. The following output returns 1. To solve such errors the callee (in this case area) should check if its result is greater than its inputs.

Narrowing conversion errors, which are a result of entering doubles instead of ints, are not caught by this program. This could be checked by letting the user enter doubles and then convert them to ints if possible (or compare them afterwards) If the user enters a double value cin gets in a bad state and the program returns without any output.

To throw if a conversion is not possible use:

int x1 = narrow_cast<int>(2.9); // throws
int x2 = narrow_cast<int>(2.0); // OK
char c1 = narrow_cast<char>(1066); // throws
char c2 = narrow_cast<char>(85); // OK

Uncaught Exception

To see what an uncaught exception error looks like, run a small program that uses error() without catching any exceptions.

uncaughterror.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Author: Franz Pucher
// Date: 2019.09.20
// Try This 5.6.3 Bad input - Uncaught error

#include "std_lib_facilities.h"


int main()
{
    // The function error throws a runtime_error
    error("Force uncaught error");
    
    return 0;
}

Running the program shows what an uncaught error looks like:

libc++abi.dylib: terminating with uncaught exception of type std::runtime_error: Force uncaught error

Process finished with exit code 6

Uncaught Exception

Get this program to run. Check that our input really does produce that output. Try to “break” the program (i.e., get it to give wrong results) by giving it other input sets. What is the least amount of input you can give it to get it to fail?

logicerrors.cpp
 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
// Author: Franz Pucher
// Date: 2019.09.20
// Try This 5.7 Logic errors

#include "std_lib_facilities.h"

// This test vector yields results that are correct.
const vector<double> temps_test_ok {
        -16.5, -23.2, -24.0, -25.7, -26.1, -18.6, -9.7, -2.4,
        7.5, 12.6, 23.8, 25.3, 28.0, 34.8, 36.7, 41.5,
        40.3, 42.6, 39.7, 35.4, 12.6, 6.5, -3.7, -14.3
};

// This test vector from the book where no values are negative gives a wrong result.
const vector<double> temps_test_wrong {
        76.5, 73.5, 71.0, 73.6, 70.1, 73.5, 77.6, 85.3,
        88.5, 91.7, 95.9, 99.2, 98.2, 100.6, 106.3, 112.4,
        110.2, 103.6, 94.9, 91.7, 88.4, 85.2, 85.4, 87.7
};


// An empty test vector is the shortest input that results in a bad output
// The size of the temps vector will be zero.
// Dividing by zero results in NaN for the average value.
const vector<double> temps_test_bad_min;

int main()
{
    vector<double> temps; // temperatures
    for (double temp; cin>>temp; ) // read and put into temps
        temps.push_back(temp);


    //for (double x : temps_test_ok)
    //    temps.push_back(x);

    //for (double x : temps_test_wrong)
    //    temps.push_back(x);

    double sum = 0;
    double high_temp = 0;
    double low_temp = 0;
    for (double x : temps)
    {
        if(x > high_temp) high_temp = x; // find high
        if(x < low_temp) low_temp = x; // find low
        sum += x; // compute sum
    }

    cout << "High temperature: " << high_temp<< '\n';
    cout << "Low temperature: " << low_temp << '\n';
    cout << "Average temperature: " << sum/temps.size() << '\n';
    
    return 0;
}

As stated in the book, the following input:

-16.5 -23.2 -24.0 -25.7 -26.1 -18.6 -9.7 -2.4
7.5 12.6 23.8 25.3 28.0 34.8 36.7 41.5
40.3 42.6 39.7 35.4 12.6 6.5 -3.7 -14.3

results in the expected an in this case correct output of:

-16.5 -23.2 -24.0 -25.7 -26.1 -18.6 -9.7 -2.4
7.5 12.6 23.8 25.3 28.0 34.8 36.7 41.5
40.3 42.6 39.7 35.4 12.6 6.5 -3.7 -14.3
|
High temperature: 42.6
Low temperature: -26.1
Average temperature: 9.29583

The next input:

76.5 73.5 71.0 73.6 70.1 73.5 77.6 85.3
88.5 91.7 95.9 99.2 98.2 100.6 106.3 112.4
110.2 103.6 94.9 91.7 88.4 85.2 85.4 87.7

yields a wrong result:

76.5 73.5 71.0 73.6 70.1 73.5 77.6 85.3
88.5 91.7 95.9 99.2 98.2 100.6 106.3 112.4
110.2 103.6 94.9 91.7 88.4 85.2 85.4 87.7
|
High temperature: 112.4
Low temperature: 0
Average temperature: 89.2083

The shortest input to “break” the program, is to enter no double. Entering no values results in a bad output because the size of the temps vector will be zero. Dividing by zero results in NaN for the average value.

|
High temperature: 0
Low temperature: 0
Average temperature: nan

Another case where the program “breaks” is when an overflow of double happens, which is basically the same error as the previous one: cin gets in a bad state and therefore the vector is empty.

1e350
High temperature: 0
Low temperature: 0
Average temperature: nan

With too high values, the average becomes inf, depending on wheater this is considered a wrong result with these high values:

1.79e308 1.79e301 1.79e302 1.79e305 1.79e308
|
High temperature: 1.79e+308
Low temperature: 0
Average temperature: inf

The logical error of initializing high_temp and low_temp with zero is also severe when only negative values are entered:

-5.0 -2.1 -3.8 -10.6
|
High temperature: 0
Low temperature: -10.6
Average temperature: -5.375

high_temp stays zero because no negative number is greater than zero.

Locic Errors

Look it up. Check some information sources to pick good values for the min_temp (the “minimum temperature”) and max_temp (the “maximum temperature”) constants for our program. Those values will determine the limits of usefulness of our program.

logicerrorsimproved.cpp
 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
// Try This 5.7 Logic errors - improved version
// Author: Franz Pucher
// Date: 2019.09.20

#include "std_lib_facilities.h"

// This test vector yields results that are correct.
const vector<double> temps_test_ok {
        -16.5, -23.2, -24.0, -25.7, -26.1, -18.6, -9.7, -2.4,
        7.5, 12.6, 23.8, 25.3, 28.0, 34.8, 36.7, 41.5,
        40.3, 42.6, 39.7, 35.4, 12.6, 6.5, -3.7, -14.3
};

// This test vector from the book where no values are negative gives a wrong result.
const vector<double> temps_test_wrong {
        76.5, 73.5, 71.0, 73.6, 70.1, 73.5, 77.6, 85.3,
        88.5, 91.7, 95.9, 99.2, 98.2, 100.6, 106.3, 112.4,
        110.2, 103.6, 94.9, 91.7, 88.4, 85.2, 85.4, 87.7
};


// An empty test vector is the shortest input that results in a bad output
// The size of the temps vector will be zero.
// Dividing by zero results in NaN for the average value.
const vector<double> temps_test_bad_min;

int main()
{
    double sum = 0;
    double high_temp = -1000; // initialize to impossibly low
    double low_temp = 1000; // initialize to “impossibly high”

    int no_of_temps = 0;
    for (double temp; cin>>temp; ) { // read temp
        ++no_of_temps; // count temperatures
        sum += temp; // compute sum
        if (temp > high_temp) high_temp = temp; // find high
        if (temp < low_temp) low_temp = temp; // find low
    }
    cout << "High temperature: " << high_temp<< '\n';
    cout << "Low temperature: " << low_temp << '\n';
    cout << "Average temperature: " << sum/no_of_temps << '\n';
    
    return 0;
}

Compared to the previous program, this improved version returns the correct results for the wrong test vector input:

-16.5 -23.2 -24.0 -25.7 -26.1 -18.6 -9.7 -2.4
7.5 12.6 23.8 25.3 28.0 34.8 36.7 41.5
40.3 42.6 39.7 35.4 12.6 6.5 -3.7 -14.3
|
High temperature: 42.6
Low temperature: -26.1
Average temperature: 9.29583

The following program uses no magic constants 1000 and -1000 for the min_temp and max_temp values. Instead, the absolute zero and absolute hot temperature values are taken:

logicerrorsimprovedmore.cpp
 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
// Author: Franz Pucher
// Date: 2019.09.20
// Try This 5.7 Logic errors - improved version

#include "std_lib_facilities.h"

// Absolute zero (ºF)
constexpr double min_temp {-459.67};


// Highest postulated temperature is the Planck temperature 1,417e+32 K
// Convert Kelvin to Fahrenheit
// (1,417e+32 K − 273,15) × 9/5 + 32
constexpr double max_temp {(1.417e+32 - 273.15) * 9.0/5.0 + 32.0};


int main()
{
    double sum = 0;
    double high_temp = min_temp; // initialize to impossibly low
    double low_temp = max_temp; // initialize to “impossibly high”

    int no_of_temps = 0;
    for (double temp; cin>>temp; ) { // read temp
        ++no_of_temps; // count temperatures
        sum += temp; // compute sum
        if (temp > high_temp) high_temp = temp; // find high
        if (temp < low_temp) low_temp = temp; // find low
    }
    cout << "High temperature: " << high_temp<< '\n';
    cout << "Low temperature: " << low_temp << '\n';
    cout << "Average temperature: " << sum/no_of_temps << '\n';
    
    return 0;
}

Another solution to this program is to initialize the min_temp and max_temp in the first iteration of the for loop. This does not require any upper and lower limits on the temperature values. However, this requries an if-statement to check the iteration of the loop:

int no_of_temps = 0;
for (double temp; cin>>temp; ) { // read temp
    ++no_of_temps; // count temperatures
    sum += temp; // compute sum
    if (1 == no_of_temps)
    {
        high_temp = temp;
        low_temp = temp;
    }
    else
    {
        if (temp > high_temp) high_temp = temp; // find high
        if (temp < low_temp) low_temp = temp; // find low
    }
}

Estimation - Hexagon Area

Our hexagon was regular with 2cm sides. Did we get that answer right? Just do the “back of the envelope” calculation. Take a piece a paper and scribble on it. Don’t feel that’s beneath you. Many famous scientists have been greatly admired for their ability to come up with an approximate answer using a pencil and the back of an envelope (or a napkin). This is an ability — a simple habit, really — that can save us a lot of time and confusion.

In a regular hexagon the lengths of each side are the same as the radius of a circumscribed circle that goes through each of the six corners. Therefore we can calculate the area of a circle to approximate the area of the hexagon. Assuming we know that the area of a circle is r^2pi (`pow(r,2)PI) the area with radius r = 2cm is 12.566cm^3. This result is reasonable, because we know that the area of the circumscribed circle is larger than that of the hexagon. In the book the value of the program that calculates the area of a hexagon is 10.3923cm^3, which is smaller than 12.566cm^3. A hexagon can be partitioned into six [equilateral triangles](https://en.wikipedia.org/wiki/Equilateral_triangle) where the area can be found using the [Pythagorean theorem](https://en.wikipedia.org/wiki/Pythagorean_theorem) to be:sqrt(3)/4r^2``. Multiplying this formula with 6 results in the exact area of the hexagon:``3sqrt(3)/2*r^2`. However, using the area of circle is a faster approximation than the exact formula.

Estimation - Driving Times

Estimate those driving times. Also, estimate the corresponding flight times (using ordinary commercial air travel). Then, try to verify your estimates by using appropriate sources, such as maps and timetables. We’d use online sources.

In this “try this” we search for the driving and flight times from New York City to Denver and from London to Nice. The estimated driving times can be calculated from air (flying) distance between the cities and an average speed that underestimates the true average speed. Both guesses (distance and average speed) should never overestimate the true values to get a useful estimation (see also admissible heuristic in path finding algorithms such as A*).

Using an online air (flying) distance calculator such as distancecalculator, we find the following distances. Note that this calculator uses the Haversine formula which determines the great-circle distance between two points on a sphere given their longitudes and latitudes.

City A City B Air distance (km) Air distance (mi)
New York Denver 2618.51 1627.07
London Nice 1027.82 638.66

We underestimate the driving speed with an average of 200km/h (124m/hr) and the flying speed (cruise)) 1000km/h (621m/hr) which gives the following driving and flying times:

City A City B Driving time Flying time
New York Denver 13h 5minutes 2h 37minutes
London Nice 5h 8minutes 1h 2minutes

To verify these driving time results we can use Google Maps:

City A City B Exact driving distance (km) Driving time Average speed(km/h)
New York Denver 2883.0 26h 111
London Nice 1396 13h 1minute 107

According to flighttime-calculator these are the true flight times between the cities:

City A City B Exact flying distance (km) Flight time Average speed(km/h)
New York Denver 2629.72 3h 31minutes 747.78
London Nice 1030.88 1h 37minutes 637.65

Post-conditions

Find a pair of values so that the pre-condition of this version of area holds, but the post-condition doesn’t.

postconditions.cpp
 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
// Try This 5.10.1 Post-conditions
// Author: Franz Pucher
// Date: 2019.09.22
//
// Comments:
//  In case of an overflow the pre-conditions are satisfied
//  while the post-condition can fail.
//  Here are some examples that produce an overflow for a 4 byte integer:
//   area(60000, 60000);
//   area(65536, 65535); // -65536
//   area(65536, 65536); // 0

#include "std_lib_facilities.h"


int area(int length, int width)
// calculate area of a rectangle;
// pre-conditions: length and width are positive
// post-condition: returns a positive value that is the area
{
    if (length<=0 || width <=0) error("area() pre-condition");
    int a = length*width;
    cout << "area() a: " << a << '\n';
    if (a<=0) error("area() post-condition"); // throw runtime_error(string s)
    return a;
}


int main()
try
{
    int length = 0;
    int width = 0;

    cout << "Enter integer length and width to get the area of the rectangle:\n"
         << "(Negative numbers will violate the pre-conditions while large numbers\n"
         << "will produce an overflow and violate the post-condition of area())\n";

    while (cin >> length >> width)
        cout << "Area is " << area(length, width) << '\n';

    return 0;
}
catch (runtime_error& e) {
    cerr << "Error: " << e.what() << '\n';
    return 1;
}
catch (...) {
    cerr << "Error: unknown exception\n";
    return 2;
}

Entering large numbers that produce an overflow satisfy the pre-conditions of area() but can violate the post-condition in case the reult is zero or negative.

Enter integer length and width to get the area of the rectangle:
(Negative numbers will violate the pre-conditions while large numbers
will produce an overflow and violate the post-condition of area())
60000 60000
Area is area() a: -694967296
Error: area() post-condition

Another input where the result would be zero because of an overflow:

Enter integer length and width to get the area of the rectangle:
(Negative numbers will violate the pre-conditions while large numbers
will produce an overflow and violate the post-condition of area())
65536 65536
Area is area() a: 0
Error: area() post-condition

Here is an input that violates the pre-condition:

Enter integer length and width to get the area of the rectangle:
(Negative numbers will violate the pre-conditions while large numbers
will produce an overflow and violate the post-condition of area())
-1 1
Area is Error: area() pre-condition

Exercises