Networked Games -- C++ (Part 2)

  • Jian Huang
  • Networked Games
  • The Notes Page

    Exception handling

    Exception handling is the built-in error handling mechanism of C++ for runtime errors. the keywords involved are: try, catch and throw.

    Program statements that you want to monitor for exceptions are contained in a try block. If an exception occurs within the block, it is thrown (using throw). The exception is caught (using catch) and then processed. The exception handling part of C++ is a lot more functional and elegant than how errors are handled in C by errno.

    try {
      // try block
    }
    catch (type1 arg) {
      // catch block 1
    }
    catch (type2 arg) {
      // catch block 2
    }
    catch (type3 arg) {
      // catch block 3
    }
    ...
    

    Obviously, for each try there can be more than one catch statement:

    Look at the following for a toy example.

    int main()
    {
    
      try {
        cout << "inside try block\n";
        throw 10;
        cout << "never gets here\n";
      }
      catch (int i) {
        cout << "caught exception. no. "<< i<
    
    

    Note the following.

    • after catch, the program execution continues, however usually exit(), abort(), etc. are called from within the catch block.
    • if in the above, "throw 10.0" is called instead, this exception is not handled, and leads to abnormal program termination from within the catch block.
    • an exception thrown from within a function called from the try block will be caught by the catch block.
    • a try block can be localized to a function. each time the function is entered, the exception handling relative to that function is reset.

    You may want catch to get all exceptions instead of just a certain type. To do so, just use:

      catch (...) {
         // process all exceptions
      }
    

    A good use is to put this form as the last catch statement in a sequence, i.e. this form becomes the default (not the original default terminate()).


    To define a throw clause to the function definition, use:

    ret-type func-name(arg-list) throw (type-list)
    {
      // ...
    }
    

    Note data types in the comma-separated type-list can be thrown by the function

    • throwing any other type of expression will cause abnormal program termination
    • Standard C++, this causes unexpected() to be called, which then calls terminate()

    So, if you say, "throw ()", then this function cannot throw any exceptions


    Furthermore, note that code in a catch block can also throw an exception. In this case, an outer try/catch sequence will catch this exception

    Now let's try to catch a few real exceptions. The first one is new. Over the years, new has been implemented in many different ways, especially considering when problems happens.

    In the current standard C++, new may fail and in fact throws an exception, bad_alloc. The convoluted mess are, however, at one time this exception was called xalloc. Additionally, the commonly assumed returning NULL is not a reliable test any more. If you don't catch this exception, your program will be terminated. To catch this exception, need to:

    #include < new >
    #include < iostream >
    using namespace std;
    
    int main()
    {
       int *p;
       
       try {
         p = new int;
       }
       catch (bad_alloc xa) {
         cout << "cannot allocate\n";
    	 return 1;
       }
    
       delete p;
    }
    

    IMPORTANT: In standard C++, there is also a form of new that returns NULL on failure, instead of throwing an exception. That form is new(nothrow):

    double * p;
    
    p = new(nothrow) double[20000];
    if (p == NULL)
      ...
    

    Run-time type idientification (RTTI)

    To obtain an object's type, use typeid:

    #include < typeinfo >
    
    typeid(object);
    

    The type_info class defines the following public members:

    bool operator== (const type_info &obj);
    bool operator!= (const type_info &obj);
    bool before(const type_info &obj); // mostly for internal purpose
    const char *name();  // returns a pointer to the name of the type
    
    typeid(type-name); // this returns a type_info boject describing the specified type,
    				   // for use in a type comparison statement
    

    typeid throws a bad_typeid exception, when it gets a bad pointer like:

       typeid (*p); // when p == NULL
    

    A more useful example is:

     
    #include < iostream >
    
    using namespace std;
    
    class base {
    
    };
    
    class derived : public base {
    
    
    };
    
    int main()
    {
      int i;
      base * p;
    
      derived d;
      p = &d;
    
      cout << "i is " << typeid(i).name() << endl;
      cout << "*p is " << typeid(*p).name() << endl;
      cout << "p is " << typeid(p).name() << endl;
      cout << "d is " << typeid(d).name() << endl;
    
      return 0;
    }
    

    The output on my Mac laptop (g++ compiler) is:

    i is i
    *p is 4base
    p is P4base
    d is 7derived
    

    A function can take a reference to a polymorphic base class. it can really get any object derived fromt he base class. using typeid, you can find out what did you really get:

    void whattype(base & ob1, base & ob2)
    {
       cout<< typeid(ob1).name() << endl;
       
       if (typeid(ob1) == typeid(ob2))
         cout << "yes, they match\n";
    	 
       if (typeid(ob1) != typeid(base))
         cout << "no, it's not the base class\n";
    }
    

    typeid can be applied to objects of a template class as well, something like:

    if (typeid(*p1) == typeid(Square < double >))
        cout << "yeah, got a double precision square\n";
    

    Type Casting

    C++ supports type casting operators defined by C, however, the proper C++ type casting are:

      dynamic_cast
      const_cast
      reinterpret_cast
      static_cast
    
    dynamic_cast tests whether a cast is valid, if not the cast fails:
    • if it's on reference types --> throw a bad_cast exception
    • if it involves pointers --> dynamic_cast evalutes to NULL

    Here is one example:

    bast * bp;
    derived * dp, d;
    
    bp = &d;
    dp = dynamic_cast< derived * > (bp);  // dynamic_cast < target-type > (expr)
    if (!dp)
       // failed
    

    dynamic_cast can also be used with template class:

    p2 = dynamic_cast< Square < double > * > (p1);   
    

    The other three casting operators are listed here as well.

    const_cast is to explicitly override const and/or volatile keywords in a cast

    • the target type and the source type must be the same
    • it only overrides const or volatile attributes, e.g. to remove const-ness

    static_cast performs a non-polymorphic cast //essentially the same as C type casting

    • it can be used to cast a base class pointer into a derived class pointer
    • no runtime checks are performed

    reinterpret_cast changes one pointer type into another fundamentally different pointer type.

    • it can also change a pointer into an integer and an int into a pointer
    • used for casting inherently incompatible pointer types

    int i;
    char *p;
    
    i = reinterpret_cast< int > (p);
    

    void func (const int * p)
    {
       int * v;
       v = const_cast< int * > (p);
       *v = 100;
    }
    

    int i;
    float f;
    
    i = static_cast< int > (f);
    

    STL

    At the core of STL are three foundational items: containers, algorithms and iterators. Let's talk about them in turn. Note, here our purpose is mainly just to know how they are used (quite straightforward actually).

    containers are objects that hold other objects, e.g. vector, queue and list classes. Containers are the STL objects that actually store data. There are also associative containers for efficient retrieval of values based on keys, e.g. map class. Each container defines a set of functions:

      - insert, delete, merge for list
      - push, pop for stack
    

    algorithms act on containers, e.g. initializing, sorting searching and transforming the contents of containers.

    iterators work more or less like pointers to cycle through the contents of a container. As there are several kinds of access that need to be supported, there are actually different kinds of iterators:

      - random access: stores and retrieves values. elements can be accessed randomly: RandIter
      - bidirectional: stores and retrieves values, forward and backward moving:       Bilter
      - forward:       stores and retrieves values, forward moving only:               ForIter
      - input:         retrieves but does not store values. forward moving only:       InIter
      - output:        stores by does not retrieve values. forward moving only:        OutIter
    

    iterators are handled like pointers, one can increment or decrement them. But do note that STL also has reverse iterators, for which incrementing means backword moving. iterators are declared using the iterator type defined by the various containers.

    STL also relies on several standard components for support. When you initialize an STL object, chances are that you may need to pick, for each of the three components, what to use.

      - allocators: each container has one defined, to manage memory allocation
                    you can define your own, but the default is usually sufficient
      - predicates: a special function (unary or binary) returning true/false.
                    the programmer specifies the condition.
      - comparison functions: a special BinPred, if arg1 < arg2
    

    The container classes in STL include:

    bitset:         a set of bits, < bitset >
    deque:          a double-ended queue < deque >
    list:           a linear list < list >
    map:            key/value pairs - each key associated with only one value, < map >
    multimap:       key/value pairs - each key associated with >= 2 values, < map >
    multiset:       a set in which each element is not necessarily unique, < set >
    priority_queue: a priority queue, < queue >
    queue:          a queue, < queue >
    set:            a set in which each element is unique < set >
    stack:          a stack < stack >
    vector:         a dynamic array < vector >
    string:         character strings < string >
    
    STL uses templates. some placeholder types are given typedef'ed names:
    size_type:  int equivalent to size_t
    reference:  a reference to an element
    const_reference: a const reference to an element
    iterator: an iterator
    const_iterator: a const iterator
    reverse_iterator: a reverse iterator
    const_reverse_iterator: a const reverse iterator
    value_type: the type of a value stored in a container
    allocator_type: the type of the allocator
    key_type: the type of a key
    key_compare: the tyep of a function that compares two keys
    value_compare: the type of a functios that compares two values
    

    Let's Learn by Example

    Given our time constraints, it would probably be too overwhelming a job for us to give a full description of how STL library is used. Fortunately, the most commonly used functionalities used in STL form a pretty small and self-explanary set that could be learned by looking at a few examples. So let's examine the following ones.

    Dynamic array is very useful in real world programming. In STL, it is implemented in < vector >.

    template > class vector {
       // Allocator defaults to the standard allocator
    };
    

    Here is how we could use it. BTW, if you need to know all the methods in a vector class, just google it. The reference page is just a click away.


    vector< int > v;
    int i;
    
    cout << v.size() << end;
    for (i = 0; i < 10; i++) v.push_back(i);
    for (i = 0; i < v.size(); i++) cout << v[i] << endl;
    
    vector::iterator p = v.begin();
    
    p += 2;
    v.insert(p, 10, 9) // insert at element[10] a value of 9
    
    while (p != v.end()) {
      cout << *p++ << endl;
    }
    
    p -= 8;
    v.erase(p, p+5); // remove the next 5 elements
    
    

    You can use your own class in a vector too:

    #include < iostream >
    #include < vector >
    
    using namespace std;
    
    class demo {
      double d;
    public:
      demo() {d = 0.;}
      demo(double x) { d = x; }
      demo &operator=(double x) {
         d = x;
             return *this;
      }
      double getd() { return d; }
    };
    
    bool operator< (demo a, demo b)
    {
      return a.getd() < b.getd();
    }
    
    bool operator==(demo a, demo b)
    {
      return a.getd() == b.getd();
    }
    
    int main()
    {
      vector < demo > v;
      int i;
      
      for (i = 0; i < 10; i ++) v.push_back(demo(i/3.0));
      for (i = 0; i < v.size(); i ++) cout<< v[i].getd() << endl;
      
      for (i = 0; i < v.size(); i ++) v[i] = v[i].getd() * 2.1;
      
      return 0;
    }
    
    

    Another really useful STL class is map, for looking up a value with a key, e.g. lookup phone numbers using names. In map, duplicate keys are not allowed, while multimap allows nonunique keys.

    template < class Key, class T, class Comp = less< Key >, class Allocator = allocator< T > > class map {
    
    };
    

    Here is a short example. Note "pair" is also a class that comes with STL. In a way, map implements a search structure for "pair".

    map< char, int > m;
    int i;
    
    for (i = 0; i < 10; i ++)
      m.insert(pair('A'+i,i));
    
    for (i = 0; i < 10; i ++)
      m.insert(make_pair((char)('a'+i),i));
    
    map< char, int >::iterator p;
    p = m.find('D');
    if (p != m.end())
      cout << p->second;
    else
      cout << "Key not found\n";
    
    

    To hold more interesting objects in map, you just need to create two classes: one for key and one for value, and a "<" operator for the key type (kinda like using qsort in C).

    class word {
      char str[20];
    public:
     word() { strcpy(str,"");}
     word(char *s) { strcpy(str,s); }
     char *get() { return str; }
    };
    
    bool operator< (word &a, word &b)
    { 
      return strcmp(a.get(), b.get()) < 0;
    }
    
    class opposite {
      char str[20];
    public:
      opposite() { strcpy(str,"");}
      opposite(char *s) { strcpy(str,s); }
      char *get() { return str; }
    };
    
    int main()
    {
      map< word, opposite > m;
      m.insert(pair(word("yes"),opposite("no")));
      ...
      
      map< word, opposite >::iterator p;
      p = m.find(word("what"));
      ...
    }
    
    

    In STL, each container provides its own basic operations. When you need more extended and complex operations, you then need to leverage algorithms. The algorithms also allow working with two different types of containers at the same time. All the algorithms are template functions, meaning that they can be applied to any type of container

    #include < algorithm > // to access STL algorithms
    

    Again, here we just look at one example. Whenever you find a functionality that you need not provided by the container class directly, first look up the reference page of STL algorithms. There is a good chance that it's provided there.

    template < class InIter, class T > size_t count(InIter start, InIter end, const T &val);
              // count number of elements beginning at start, ending at end that match val
    		
    template < class InIter, class T > size_t count_if(InIter start, InIter end, UnPred pfn);
              // count number of elements beginning at start, ending at end that unary 
              // predicate pfn returns true
    

    These just do a count from start to end, for elements matching a target value (count()) or meeting a criterion/prediate (count_if()).

    #include < iostream >
    #include < vector >
    #include < algorithm >
    
    using namespace std;
    
    bool even(int x)
    { 
      return !(x%2);
    }
    
    int main(void)
    {
      vector < int > v;
      int i, n;
    
      for (i = 0; i < 20; i ++) {
        if (i%2) v.push_back(1);
        else v.push_back(2);
      }
      
      n = count(v.begin(), v.end(), 0);
      cout <<"count 0: "<< n << endl;
      n = count(v.begin(), v.end(), 1);
      cout <<"count 1: "<< n << endl;
      n = count_if(v.begin(), v.end(), even);
      cout <<"count even: "<< n << endl;
    
      return 0;
    }  
    

    Output:

    count 0: 0
    count 1: 10
    count even: 10