Data Abstraction
- Function of a data type is more important than the physical
characteristics
- Imperative languages don't provide this feature
- Function members. A language such as C++ provides and ideal
tool for data abstraction.
- Definition of the class (.HPP file) is separated from its
implementation (.CPP file)
- The users of the data types don't have to and possibly not
allowed to look at the implementation (.CPP).
- The data manipulation should be done through the function
members
- Information hiding is generally part of a good abstraction
- public, private and protected (is relevant in the context
of inheritance)
- By default all the members in a class definition are private.
- Various features of C++ as an Object Oriented Language
- Encapsulation
- Inheritance
- Polymorphism and its relation to the templates
- C++ is a better C and chapter 6 looks at many of these features
which are not related to the OOP
- constructor, destructor, function overloading, operator overloading
- How these features are implemented in C++
- Exception handling and OO exception handling
- Writing a C++ compiler is easy as long as you don't include
templates and exception handling. Once you include those it becomes
a messy affair.
- Constructor
- Same name as the data type
- Every variable needs to be initialized
- a function such as initialize() was a common function in imperative
programming
- what if the user forget to call initialize()
- constructor doesn't leave it upto the user
- constructor is called automatically when the variable is created
- destructor is called automatically when the variable goes
out of scope. important when the variable collects memory dynamically
such as linked list. you want to return the memory back to the
OS
- function overloading
- suppose we want to have functions to write int, double, etc.
- C/Pascal will make you use awkward names such as write_int(int
j), write_double(double d), etc.
- We can use the same name write(int j), write(double d)
- Both of these functions are indeed different but we didn't
have to be creative in choosing the names
- operator overloading
- same as function overloading
- matrix a, b, c;
- c = mult(a,b)
- c = a * b // more natural way of writing
OO features of C++
- Encapsulation
- function and data members instead of just data members
- templates
- instead of defining the class we define a template that can
be used to create the class
- Templates in C++ are not translated by compiler
- Because templates don't specify important details such as
the type of the components. So the compiler cannot decide memory
allocation
- Templates can be specified for either a class or a function
- We have seen a lot of examples for classes. Let us look at
examples for functions.
- Passing functions, templates and polymorphism can be interchangeable:
compare.cpp, template.cpp, polymorp.cpp.
- All of them of the same thing
- Question is which is the best way. Answer depends on the application
- Assignment #3. Passing functions is clearly the right thing
to do
- sorting template of a function is the right thing to do
- The animal example: if we had more complicated functions,
probably polymorphism will be the way to go. But for limited functionality
passing functions seems simplest.
- Keep it simple, as simple as possible, no simpler -Einstein
- Polymorphism is used along with inheritance
- polymorp.cpp we have a type called animal which has two members:
irritation() and sound()
- sound() is a virtual function or is polymorphic. can take
different faces (hence the word polymorphic from Greek(?))
- when irritation is going to call it will decide which sound
to call depending upon the object.
- if we drop the word virtual see what happens
- The sound function from the animal is called all the time
- If a function is not virtual, a call to the function is translated
by hard coding the address of the function
- When irritation is called it will hard code the address of
the animal.sound(). Even if we call irritation from dog or cat
we will be going to the same hard coded address
- what does the word virtual do?
- It tells the compiler to not hard code the address of sound().
It is left blank
- The blank is filled by looking at the most recently defined
sound()
- For dog, the blank is filled by dog::sound()
- The address of the function is resolved at run-time instead
of compile time using a VMT (virtual method table) associated
with the object type in this case dog
- Assumption: You know the basic concept of inheritance
- Since dog is derived from animal, it gets all the members
from animal (inheritance)
- How many members does dog have?
Exception handling
- throw(), catch(), try are the three concepts used in C++ for
exception handling
- what throw does is: throws a variable
- catch catches the variable within a try block
#include <iostream.h>
class divide_by_zero {};
double avg(double a[], int size)
{
int sum=0;
for(int i = 0; i < size; i++)
sum += a[i];
if(size) return sum/size;
throw divide_by_zero();
}
void main()
{
try
{
double y[5];
y[0]=17.89;y[1]=10.9;y[2]=3.89;y[3]=1.40;y[4]=8.66;
avg(y,0);
}catch(divide_by_zero)
{
cerr << "Trying to divide by zero";
}
}
- We don't need complicated and awkward programming that passes
error codes around
- When we are running a segment of program we don't need to
keep on checking for the errors after execution of each statement.
All of the error checking is taken care of by one try and catch
construct.
- Named exceptions:
include <iostream.h>
enum illegal_operation {divide_by_zero, invalid_pointer};
double avg(double a[], int size)
{
int sum;
if(a == 0) throw invalid_pointer;
for(int i = 0; i < size; i++)
sum += a[i];
if(size) return sum/size;
throw divide_by_zero;
}
void main()
{
try
{
double y[5];
y[0]=17.89;y[1]=10.9;y[2]=3.89;y[3]=1.40;y[4]=8.66;
avg(0,10);
avg(y,0);
}catch(illegal_operation i)
{
switch(i)
{
case divide_by_zero:
cerr << "Trying to divide by zero";
break;
case invalid_pointer:
cerr << "Trying to dereference invalid pointer\n";
}
}
}
- The variable that is thrown can be caught and examined. Naming
the exception means the exception that is thrown is given a name.
In this case the exception is caught and given a name called I.
- Exception can be organized in a hierarchy using inheritance
#include <iostream.h>
class illegal_operation {};
class divide_by_zero:public illegal_operation{};
class invalid_pointer:public illegal_operation{};
double avg(double a[], int size)
{
int sum;
if(a == 0) throw invalid_pointer();
for(int i = 0; i < size; i++)
sum += a[i];
if(size) return sum/size;
throw divide_by_zero();
}
void main()
{
try
{
double y[5];
y[0]=17.89;y[1]=10.9;y[2]=3.89;y[3]=1.40;y[4]=8.66;
//avg(0,10);
avg(y,0);
}catch( illegal_operation)
{
cerr << "Trying to do something illegal";
}
catch (invalid_pointer)
{
cerr << "Illegal pointer\n";
}
catch(divide_by_zero)
{
cerr << "Divide by zero\n";
}
}
- In the above case when a variable of either of the two derived
classes is thrown, we will catch it as illegal_operation so the
last two catches are useless.
- Inheritance corresponds to an is_a relationship
- A varibel of the type invalid_pointer or divide_by_zero is
a illegal_operation as well
- Looking at another example since dog, cat, duck are derived
from animal, a variable of the type dog can be sent to a function
that expects a variable of the type animal. A dog is an animal,
a cat is an animal, a duck is an animal.
#include <iostream.h>
struct illegal_operation
{
virtual void debug_print() { cerr << "Illegal Operation\n";}
};
struct divide_by_zero:public illegal_operation
{
void debug_print() { cerr << "Divide by zero\n";}
};
struct invalid_pointer:public illegal_operation
{
void debug_print() { cerr << "Invalid pointer\n";}
};
double avg(double a[], int size)
{
int sum;
if(a == 0) throw invalid_pointer();
for(int i = 0; i < size; i++)
sum += a[i];
if(size) return sum/size;
throw divide_by_zero();
}
void main()
{
try
{
double y[5];
y[0]=17.89;y[1]=10.9;y[2]=3.89;y[3]=1.40;y[4]=8.66;
//avg(0,10);
avg(y,0);
}catch( illegal_operation& i)
{
i.debug_print();
}
}
- We used polymorphism in the above version. So all we do is
catch the varibale of the type illegal_operation and call the
virtual function debug_print. It will call the appropriate version
of the debug_print.
- If either the virtual word or the & sign was missing,
we will get "Illegal Operation" as the error message
- why did it happen when the word virtual was dropped (quiz)
- whe we drop the &, we are passing the variable to catch
by value. That means a copy of divide_by_zero variable is made
to another variable of the type illegal_operation. Extra information
in the derived class is lost in this copy.
- After using this inheritance and polymorphism we managed to
get back to our original program where illegal operation was an
enumerated type
- In fact there is even an easier way of doing the same thing
#include <iostream.h>
double avg(double a[], int size)
{
int sum;
if(a == 0) throw "invalid_pointer";
for(int i = 0; i < size; i++)
sum += a[i];
if(size) return sum/size;
throw "divide_by_zero";
}
void main()
{
try
{
double y[5];
y[0]=17.89;y[1]=10.9;y[2]=3.89;y[3]=1.40;y[4]=8.66;
//avg(0,10);
avg(y,0);
}catch( char i[])
{
cerr << i << endl;
}
}
- We throw an array of characters. No need to write exception
classes.
- Is it good to not use the OO exception handling?
- Depends upon the applications
- Sometimes it makes sense to group excpetions using inheritance
and polymorphism
- Exceptions which are not errors
- Assignment #5. stack_empty and stack_full are not errors they
are just exceptional circumstances which may need different code
to handle them
- Concurrent programming
- let us say we have two processes running in parallel accessing
one stack. Let us say stack has is_empty(), is_full(), pop(),
peek(), push() operations which are implemented as atomic operations.
That means when one process is executing is_empty() no other process
can access the stack.
- stack has one item left
- first process comes in checks the function is_empty() which
returns false. Before the first process had a chance to pop the
item, second process sneaks in and checks if the stack is empty.
Of course it is not empty. First process then pops the item, but
second process continues believe that the stack is not empty and
tries to pop an item.
- Instead of having is_empty() as a function if we use it as
an exception. pop() operation which is atomic will throw the is_empty
exception
- We have left more than half the features of exception handling
unexplored
Concluding remarks
- We have seen that many of the OOP tricks can be implemented
using non-OOP techniques. Many times the resulting program may
even be simpler with non-OOP techniques. See the compare.cpp versus
polymorp.cpp
- We want simple solutions
- But we have to look at the modelling the real world
- cat function that we used in compare.cpp should not be a function.
cat is a class of objects and it is derived from animal. So if
we want a good representation of real life entities we may have
to use OOP
- In complicated programs, and reduce, reuse, recycle philosophy
you may be better off with OOP
- Any language can be use to write OO programs, the question
is how easy it is to write OO programs in that language. C++ fairs
better than C, Pascal, Fortran