CS202 Lecture notes -- Polymorphism and an Introduction to Exceptions

  • Jim Plank
  • Directory: ~jplank/cs202/notes/String
  • Lecture notes: http://web.eecs.utk.edu/~jplank/plank/classes/cs202/Notes/String
  • Original Notes: Sometime in the 1990's
  • Latest Revision: Mon Sep 13 14:56:52 EDT 2021

    Dealing with C style strings

    In this class, you should use C++ strings for everything, but be aware that you will have to deal with C-style strings in a few situations:


    The String Methods Find() and Substr(), plus Polymorphism

    Strings have a method find() that allows you to look for characters and substrings. You can call it in a variety of ways. Find() is a method that allows you to find characters or substrings within strings. Read the reference page from www.cppreference.com. It defines four find() methods, and these can have multiple sets of parameters. The program src/string-find.cpp illustrates all of those parameter combinations:

    /* This program demonstrates all of the ways to call the find() method of strings. */
    
    #include <iostream>
    #include <string>
    #include <cstdlib>
    #include <cstdio>
    using namespace std;
    
    int main()
    {
      string a, b;
      size_t i;
    
      /* Set two strings to use as examples. */
    
      a = "Lighting Strikes.  Lightning Strikes Again.";
      b = "Light";
    
      /* Print out the strings with digits over the top, so that it's easier to see the digits. */
    
      cout << "    ";
      for (i = 0; i < a.size(); i++) cout << i%10;
      cout << endl;
    
      cout << "a = " << a << endl;
      cout << "b = " << b << endl;
      cout << endl;
    
      /* We call a.find() in a variety of ways.  Ignore the printf statements, because they
         are a little confusing.  Just look at the calls on the right. */
    
      printf("a.find(b) = %ld\n",               a.find(b));
      printf("a.find(b, 1) = %ld\n",            a.find(b, 1));
      printf("a.find(b, 20) = %ld\n",           a.find(b, 20));
      printf("a.find('g') = %ld\n",             a.find('g'));
      printf("a.find('g', 20) = %ld\n",         a.find('g', 20));
      printf("a.find(\"Strike\") = %ld\n",      a.find("Strike"));
      printf("a.find(\"Strike\", 20) = %ld\n",  a.find("Strike", 20));
      printf("a.find(\"Aging\", 0, 2) = %ld\n", a.find("Aging", 0, 2));
      printf("\n");
    
      printf("string::npos = %ld\n", string::npos);
      return 0;
    }
    

    The first three find() calls illustrate finding a C++ string within a string. It returns the index of the first occurrence of the substring. If you call find() with a second argument, it says to start looking at that index. The first occurrence of "Light" starting from character 1 is at character 19. If find() fails, it returns string::npos, which is in reality -1. However, you should use string::npos rather than -1 to make your programs more portable.

    The next two find()'s show finding a character, and the next two show finding a C style substring. The last one shows that if you give it a C style substring, a starting index and a third argument -- count-- it will only look for count characters of the substring. Thus, even though "Aging" doesn't appear in the string, we're only looking for the first two characters -- "Ag" -- which occur at index 37.

    UNIX> bin/string-find
        0123456789012345678901234567890123456789012
    a = Lighting Strikes.  Lightning Strikes Again.
    b = Light
    
    a.find(b) = 0
    a.find(b, 1) = 19
    a.find(b, 20) = -1
    a.find('g') = 2
    a.find('g', 20) = 21
    a.find("Strike") = 9
    a.find("Strike", 20) = 29
    a.find("Aging", 0, 2) = 37
    
    string::npos = -1
    UNIX> 
    
    The feature of C++ that lets you define multiple instances of a procedure or method that work on multiple types of arguments is called polymorphism. If you give a combination of arguments that is not supported, then you will get a compilation error. For example, in src/bad-find.cpp we make a seemingly innocuous call of "a.find(b, 1, 3)":

    /* This shows how you get a compiler error if you make a find() call with the wrong
       types of arguments.  In this instance, you are trying to call:
    
       size_type find(const string &str, size_type pos, size_type count);
    
       However, that combination of parameters is not supported. */
    
    #include <iostream>
    using namespace std;
    
    int main()
    {
      string a, b;
      int i;
    
      a = "Lighting Strikes.  Lightning Strikes Again.";
      b = "Light";
    
      i = a.find(b, 1, 3);
      return 0;
    }
    

    This doesn't compile, because there is no definition of find(string, int, int). There are the following definitions:

    None of them match, so you get a compiler error:
    UNIX> g++ src/bad-find.cpp
    bad-find.cpp:19:9: error: no matching member function for call to 'find'
      i = a.find(b, 1, 3);
    ......
    UNIX>
    

    Substr() is a method that takes a starting index and an optional count, and returns a substring of a string. The simple example program is src/string-sub.cpp

    /* This program demonstrates the substr() method of strings. */
    
    #include <iostream>
    using namespace std;
    
    int main()
    {
      string a;
      size_t i;
    
      a = "Lighting Strikes.  Lightning Strikes Again.";
    
      /* Print out digits, so that it's easier to see the indices of the string. */
    
      cout << "    ";
      for (i = 0; i < a.size(); i++) cout << i%10;
      cout << endl;
    
      /* Now make a few a.substr() calls. */
    
      cout << "a = "                           << a << endl;
      cout << "a.substr(19) = "                << a.substr(19) << endl;
      cout << "a.substr(19, 13) = "            << a.substr(19, 13) << endl;
      cout << "a.substr(19, 13).substr(5) = "  << a.substr(19, 13).substr(5) << endl;
    
      return 0;
    }
    

    When only one argument is given to substr(), it returns a substring from the given index to the end of the string. If two arguments are given, it returns the specified number of characters. Since the substring is a string, you can call its methods, such as c_str() and substr().

    UNIX> string-sub
        0123456789012345678901234567890123456789012
    a = Lighting Strikes.  Lightning Strikes Again.
    a.substr(19) = Lightning Strikes Again.
    a.substr(19, 13) = Lightning Str
    a.substr(19, 13).substr(5) = ning Str
    UNIX> 
    

    Introduction to Exceptions

    As the name implies, exceptions are meant to handle unexpected things in your programs. I am not a big fan of their unbridled use, but they can really simplify your code when it comes to handling errors, so I highly recommend their use for that purpose. The way to use an exception is to put the code that you care about inside a "try" clause. When you discover an error, you call throw(), passing it an argument. At the end of your "try" clause, you can then have one or more catch() clauses. The "catch" clause must specify the type of the argument thrown by throw(). The control flow goes directly from the throw() to the catch(), skipping all code in between. Let's exemplify, in src/ex1.cpp:

    /* This is a program to demonstrate using exceptions to handle errors.
       We process the command line, and when we get an error, we throw an
       exception, passing a string to the "throw" call. When you throw the
       exception, the control goes to a "catch" clause that specifies the
       type of the thrown exception. */
    
    #include <iostream>
    #include <cstdio>
    #include <sstream>
    using namespace std;
    
    int main(int argc, char **argv)
    {
      int a, b, c;
      istringstream ss;
    
      /* Process the command line, and when you see something wrong, you throw
         an exception.  Since the quoted strings are actually C-style strings, you 
         need to typecast them to C++ strings when you throw them. */
    
      try {
        if (argc != 4) throw((string) "usage: bin/ex1 a b c");
    
        ss.clear(); ss.str(argv[1]); if (!(ss >> a)) throw((string) "a is not an integer.");
        ss.clear(); ss.str(argv[2]); if (!(ss >> b)) throw((string) "b is not an integer.");
        ss.clear(); ss.str(argv[3]); if (!(ss >> c)) throw((string) "c is not an integer.");
    
      /* Here's where you "catch" a thrown exception. */
    
      } catch (string s) {
        cerr << s << endl;
        return 1;
      }
    
      /* If the "try" clause was successful, you'll end up here, skipping the "catch" code. */
    
      printf("a = %d\n", a);
      printf("b = %d\n", b);
      printf("c = %d\n", c);
      return 0;
    }
    

    When we run this, you can see that whenever we discover an error, the control goes directly to the catch() code. If there's no error, the catch() code is skipped:

    UNIX> bin/ex1
    usage: bin/ex1 a b c
    UNIX> bin/ex1 no numbers here
    a is not an integer.
    UNIX> bin/ex1 55 fred 77
    b is not an integer.
    UNIX> bin/ex1 55 66 77 
    a = 55
    b = 66
    c = 77
    UNIX> 
    
    You'll note that I did a typecast of the quoted string to a C++ string. If you don't do that, you'll need to catch a const char *, or you won't catch the exception. In src/ex2.cpp, I don't do the typecast, and as you see, the exception is not caught:
    UNIX> bin/ex2
    libc++abi.dylib: terminating with uncaught exception of type char const*
    Abort
    UNIX> 
    
    You can specify a catch that catches anything with:

    catch (...)
    

    You don't get the argument when you do this.


    Catching exceptions from procedures and methods

    You don't have to catch an exception in the same try/catch clause. For example, you can call a procedure, and catch its exception, if it doesn't catch it. Here's a very simple example, in src/ex3.cpp. The procedure a() throws an exception, which it does not catch. Instead, it is caught by main().

    /* This demonstrates catching an exception thrown by a procedure call. */
    
    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    /* The procedure a() throws an exception that it does not catch. */
    
    void a()
    {
      printf("A is called, and is going to throw an exception of type int.\n");
      throw(5);
      printf("This code does not get called.\n");
    }
    
    /* Instead, main() catches the exception and prints out its argument. */
    
    int main()
    {
      try {
        printf("Calling a()\n");
        a();
        printf("A did not return, did it?\n");
    
      } catch (int i) {
        printf("I caught an integer: %d\n", i);
      }
    
      return 0;
    }
    

    You should be able to trace through the code here to see the following output:

    UNIX> bin/ex3
    Calling a()
    A is called, and is going to throw an exception of type int.
    I caught an integer: 5
    UNIX> 
    
    If you are nested very deep in a sequence of procedure and method calls, and you throw an exception, it will be caught by the closest caller who catches that type of exception. If no one catches it, then you'll see an error like the one with ex2.cpp above.

    There are interesting interactions with exceptions and destructors. Stay tuned to future lectures for that.


    My advice with exceptions

    As you can imagine, exceptions can lead to pretty unreadable code. For years, I pretended that they didn't exist, because I don't like unreadable code, but I now believe that when used correctly, exceptions clean up your code and make it more readable. And I'm all for that. Here is my advice concerning exceptions:

    Random Numbers

    I don't go over this in class, but I have material here for you as a reference in case you want to use the MOA RNG.

    The issue of random numbers is a thorny one. C's rand() has undergone a number of changes and is not a very good random number generator. I don't believe that drand48()/lrand48() has changed over time, but it's a pretty bad random number generator. I don't recommend that you use lrand48(), because it is so bad. C++11 implements a variety of random number generators -- go ahead and read src/https://en.cppreference.com/w/cpp/numeric/random if you care. They are a little clunky for my tastes, and I've been burned by underlying RNG implementations changing, so I don't use them. However, that doesn't preclude you from using them, so feel free to.

    What I'm going to do in this class is use a header-only RNG library that my research group uses. It's called the "Mother of All" RNG, and the original code is available on Github at src/https://github.com/mfoo/Math-Library-Test/blob/master/src/mother.cpp. It is open source, so use it however you want. When I use it in a set of lecture notes, I'll include the header file in the lecture note directory. In this directory, it is in include/MOA.hpp. Here are the relevant parts of the class definition:

    /* These are wrappers around the "Mother of All" random number generator from 
       http://www.agner.org/random/.
     */
    
    #pragma once
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <cstdint>
    
    class MOA {
      public:
        double   Random_Double();                /* Returns a double in the interval [0, 1) */
        double   Random_DoubleI();               /* Returns a double in the interval [0, 1] */
        int      Random_Integer();               /* Returns an integer between 0 and 2^31-1 */
        uint32_t Random_32();                    /* Returns a random 32-bit number */
        uint64_t Random_64();                    /* Returns a random 64-bit number */
        void     Random_128(uint64_t *x);        /* Returns a random 128-bit number */
        uint32_t Random_W(int w, int zero_ok);   /* Returns a random w-bit number. (w <= 32)*/
        void     Fill_Random_Region (void *reg, int size);   /* reg should be aligned to 4 bytes, but
                                                                       size can be anything. */
        void     Seed(uint32_t seed);            /* Seed the RNG */
    

    To demonstrate its simple use, the program src/gendouble.cpp reads a number from standard input, and then generates that many random doubles, between 0 and 1:

    /* This program uses the Mother of All random number generator from MOA.hpp to print
       a bunch of random doubles between 0 and 1. */
    
    #include "MOA.hpp"
    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    int main()
    {
      MOA rng;
      double d;
      int n, i;
    
      /* Read a value, n, from standard input. */
    
      if (!(cin >> n)) return 1;
    
      /* Generate n random doubles and print them. */
    
      rng.Seed(0);
      for (i = 0; i < n; i++) {
        d = rng.Random_Double();
        printf("%7.5lf\n", d);
      }
    
      return 0;
    }
    

    It works as expected, and is pretty unexciting. Since I set the seed to zero, you get the same random numbers every time. If you don't set the seed, it sets the seed to the current time (in seconds), so you get a different sequence each time you run the program.

    UNIX> echo 2 | bin/gendouble
    0.33661
    0.58429
    UNIX> echo 4 | bin/gendouble
    0.33661
    0.58429
    0.33730
    0.79562
    UNIX> 
    
    The MOA generator is pretty simple and fast.