C++ Concepts
Selected Upgrades

Daniel Sigg, May 2001


  1. Overview
  2. Function and Operator Overloading
  3. Classes
  4. Template 1-2-3
  5. Operators
  6. STL (Standard Template Library)
  7. Namespaces
  8. Excpetions


Overview

Books:
Stanley B. Lippman, and Josee Lajoie, "C++ Primer".
Bjarne Stroustrup, "The C++ Programming Language".
Nicolai M. Josuttis, "The C++ Standard Library".

C++ is derived from C and inherits all its basic features but adds features such as classes, templates, function overloading, operator overloading and a standard template library (STL) containing a set of generic containers and algorithms.



Function Overloading

Function overloading refers to the fact that in C++ different functions can have the same name as long as their argument list is different. For example

void Print (int n);
void Print (double x, int precision = 6);
void Print (const char* p);
are all valid. Function overaloading also applies to methods of a class (see next section).



Classes

Introduction

A class (or object) is a combination of data records and functions (called methods). It puts both together in way which should enable the implementation of data independent interfaces (ar access methods). An Example:

class basic_filter {    // define a class named basic_filter
   public:              // the public interface is accessible from outside
      enum filtertype {    // this is a public type
         FIR = 0,
         IIR
      };
      // method to get the filter type
      filtertype getFilterType() const {
       // use const for methods which don't change data members!
         return fFilterType; }
      // method to set the filter type
      void setFilterType (filtertype ftype) {
         fFilterType = ftype; }
   private:             // only accessible by class methods
      // data member: filter type
      filtertype fFilterType;
   };
A class should behave like any ordinary data type, i.e.,
basic_filter  myfilter;                  // Declaration
basic_filter filter2 = myfilter;         // Declaration and initialization
filter2 = myfilter;                      // Assignment/Copy
basic_filter filter3 = new basic_filter; // Creation and initialization
delete filter3;                          // Destruction
The above class is not complete and wouldn't work in practice, since it misses an initialization method--generally called a constructor. On the other hand a class has a default cleanup method (or destuctor) which does nothing, a default copy (or assignment) operator which copies the data memebers one by one and a default copy constructor which initalizes the object by copying the data members from an other object of the same type. To define a constructor we add the following methods:
class basic_filter {
   public:
      basic_filter () : fFilterType (FIR) {}
      explicit basic_filter (filtertype ftype) : fFilterType (ftype) {}
      ...
   };
The above constructor demonstrate several important features:
  • A constructor has the same name as the class,
  • Members can be initalized after a colon using the bracket notation,
  • Multiple methods (and functions) can have the same name as long as their argument list differs (overloading),
  • Constructors with one argument should use explicit to avoid automatic type conversions,
  • Constructors don't have a return arguments.

  • If you need a destructor declare it with
    ~basic_filter ();
    Inheritance

    One key feature of object oriented design is inheritance. In the above example we defined a class basic_filter with the idea to later derive new classes of type IIIRFilter and FIRFilter which inherit both data members and methods of the basic filter type. This has two big advantages: (i) we have to write the code which is common for both filter classes only once, and (ii) we can use the basic_filter class as an abstract class which defines the interface common to all derived filter classes. This way a user which gets passed a filter class only has to know about basic_filter regardless of the actual implementation. Example:

    class basic_filter {
       public:
          ...
          // Define basic data type for filter calculation
          typedef float datatype;
          // Managing a filter history is common
          void GetHistory (const datatype*& hist, int& N) const;
       protected:
          // Only derived classes can set the history
          void SetHistory (const datatype* hist, int N);
       public:
          // Applying a filter is a common task (interface)
          virtual TSeries Apply (const TSeries& ts) = 0;
          // Just for the fun of it we also overload the apply operator
          TSeries operator() (const TSeries& ts) {
             return Apply (ts); }
          ...
       private:
          // Data members for filter history
          int fHistoryLen;
          datatype* fHistory;
          ...
    };
    // IIRFilter inherits from basic_filter
    class IIRFilter : public basic_filter {
       public:
          // Must define a new constructor
          IIRFilter (complex<double>* poles, int pnum,
                     complex<double>* zeros, int znum, double gain);
          // Must define an Apply method
          virtual TSeries Apply (const TSeries& ts);
       ...
    };
    // FIRFilter inherits from basic filter as well
    class FIRFilter : public basic_filter {
       public:
          // Must define a new constructor and Apply method
          ...
    };
    To see how this all works in practice let's look at a few statements which use these filter classes:
    basic_filter bf1;      // Invalid: this is an abstract class
    basic_filter* pf;      // Valid: pointer to a filter
    IIRFilter if (...);    // Valid IIR filter
    FIRFilter ff (...);    // Valid FIR filter
    basic_filter& rf(&if); // Valid: reference to an IIRFilter
    pf = &if;              // Valid
    pf->GetHistory(...)    // Calls basic_filter::GetHistory(...)
    if.GetHistory(...)     // Calls basic_filter::GetHistory(...)
    TSeries inp, out;
    out = ff.Apply(inp);   // Calls FIRFilter::Apply(inp)
    out = pf->Apply(inp);  // Calls IIRFilter::Apply(inp)
    out = ff(inp);         // Calls basic_filter(inp) which calls FIRFilter::Apply(inp)
    out = rf(inp);         // Calls basic_filter(inp) which calls IIRFilter::Apply(inp)
    The keyword virtual is used to indicate a method which will be overriden by descendents and which will be resolve at run-time. The virtual Apply method in basic_filter is a pure vurtual method--indicated by setting it equal zero. This is not absolutely necessary, we could have just defined a method which was doing nothing. However, any class which has a pure virtual method defined can not be instantinated; this enforces that all descnedents must override this method and that it isn't possible to declare a basic_filter object.

    Why all the trouble one may ask? Imagine yet another class FilterBank which manages sets of filters and applies them to an incoming data stream. FilterBank only has to know about basic_filter since it does only apply filters (they are created elsewhere). Using the basic_filter class (or better a list of pointers referencing filters which are derived from basic_filter), the filter bank can be written without any knowlegde how a filter may be implemented or how many different filter implementation eventually will be written! For example, one can easily imagine the following FilterBank class:

    class FilterBank {
       public:
          FilterBank(); // Initalizes the filter bank
          void AddFilter (basic_filter* filter); // Adds a filter to the bank
          void AddTrigger (basic_trigger* trigger); // Same tick for the trigger!
          EventList Apply (const TSeries& ts);
             // Applies the filters, evaulates the trigger and returns an event list
       ...
       private:
          int fFilterNum; // Number of filters
          basic_filter** fFilterBank; // List of filter pointers
          basic_trigger* fTrigger; // Pointer to a trigger
       ...
    }
    Cleanup and copy revised

    The alerted reader may have noticed that the introduction of the history buffer introduced a memory leak when the object is no longer used and that the default member-wise copy constructor and assignment operator only copy the pointer to the buffer rather than its contents. To make the basic_filter class work again we have to add the following methods:

    class basic_filter {
       public:
          ...
          // Copy constructor
          basic_filter (const basic_filter& filter) : fHistory (0) {
             *this = filter; }
          // Cleanup (destructor)
          virtual ~basic_filter() {
             delete [] fHistory; }
          // Assignment operator
          basic_filter& operator= (const basic_filte& filter) {
             if (this != &filter) {
                fFilterType = filter.fFilterType;
                fHistoryLen = filter.fHistoryLen;
                if (fHistory) delete [] fHistory;
                fHistory = 0;
                if (fHistoryLen > 0) {
                   fHistory = new float [fHistoryLen];
                   for (int i = 0; i < fHistoryLen; ++i) fHistory[i] = filter.fHistory[i];
                }
             }
             return *this;
          }
       ...
    };
    Notice the use of the this pointer; this is an implicit argument to all methods and points to the actual object in memory. Also notice that the detsuctor has been declared virtual; this is necessary to guarantee that the correct cleanup method is called when derived classes have been defined.



    Template 1-2-3

    Templates are classes or functions which use types as arguments. Templates are rarely defined by the user since they are usually part of an object library. However, it is important to know how to use them. In the basic_filter class we had to decide on the datatype. What if we later change our opinion and alos want to have filters which work on doubles? Obviously this is easy done by cpoying the code, renaming all class and definind datatype as double. A lot of work for a straight forward matter one might think. Indeed, templates allow us to anticipate the need and leave the exact type open until the class is needed. Example:

    template <typename T>  // Templates use angle brackets
    class basic_filter {
       public:
          typedef T datatype;
       ... // rest stays the same
    };
    Similary one would define a template for the IIRFilter class by
    template <typename T>
    class IIRFilter : public basic_filter<T> {
       ... // rest stays the same
    };
    The main difference is when a template class is used, one has to specify the type of the template using angle brackets, i.e.,
    basic_filter<double>* pf;  // declares a pointer to basic_filter with doubles
    IIRFilter<float> if;       // declares an IIRFilter besed on floats
    One of the instances where templates where already used in the previous section were the complex numbers. C++ comes with a complex class which is implemented as a template. As a matter of fact the main importants of templates stems from their heavy use in the standard template library which is part of C++ (see next section).



    Operators

    C++ allows to overload standard arithemtic operators. We have already seen examples of the assignment and apply operator, but the scheme extens to almost all operators. As an example we define the plus operator for time series which concatenates the two series together (if their adjacent), i.e.,

    TSeries operator+ (const TSeries& t1, const TSeries& t2) {
       ...
    }


    STL (Standard Template Library)

    STL is an imortant part of C++ but it is beyond the scope of the introduciton to give a detailed explanation. STL implements template for containers such as doubly linked lists, arrays of objects, binary trees, sets, stacks, etc. Since these are templates the user can define a doubly linked list of its own classes simply by defining

    list<myClass> x;
    These containers come with a full set of access and modify methods, iterators and algorithms. A programmer who needs to manage a set of classes in one form or the other should take a closer look to see if what he needs is already provided rather than reinvent it again.

    STL also contains such useful class as string (array of characters), complex (for complex number arithmetic) and IO streams (for input and output).



    Namespaces

    Namesapces were introduced to prevent common names defined in different libraries from clashing. One places headers and program code into a namespace by surrounding them with

    namespace SignalProcessing { // introduce the SignalProcessing namespace
    class basic_filter {         // filter class definitions
    ...
    }                            // end of namespace
    Now using the filters from outside the SignalProcessing namespace requires either a using directive or a fully qualified name
    using namespace SignalProcessing;     // Import everything from SignalProcessing
    using SignalProcessing::basic_filter; // Import basic_filter only
    SignalProcessing::basic_filter* pf;   // Use fully qualified name in declaration
    There are two predefined namespaces: standard (std) and general (no name). STL is defined in std.



    Exceptions

    The idea is to provide a mechanism to rise excpetions when something out-of-the-ordinary happens (such as an error) and allow the programmer to catch it over a whole block of code at once. Example:

    // Try block
    try {
       basic_filter* filter = new IIRFilter (...);
       out = filter->Apply (inp);
    }
    // Catch block
    catch (bad_alloc) { cout << "Out of Memory" << endl; }
    catch (...) { cout << "Unknown Error" << endl; throw; } // rethrow
    ...
    Exceptions are a software construct. Hardware excpetions such as memory violations will still terminate the program if not managed by a signal handler. The usefulness of excpetions is debated. They are usually highly regarded by so-called software designers whereas down-to-earth programers ask themselves wouldn't it be better to develop programs that don't fail.