Courses/CS 2124/Lab Manual/Virtual Methods and Polymorphism

From A-State Computer Science Wiki
Jump to: navigation, search

Introduction

This lab illustrates how virtual functions are used to evaluate function calls based on the type of the object involved. To do so, the example from the previous lab is continued.

Createproject.pngCreate a project oop10; you will create the files listed below; upon completion of the in-lab portion of this assignment, submit all of these files in zip file oop10iL.zip.
File Description
main.cpp main program
Point2D.h & Point2D.cpp 2-D Cartesian coordianate
Shape.h & Shape.cpp base class for all geometric shapes
Circle.h & Circle.cpp descendant of Shape
Polygon.h & Polygon.cpp descendant of Shape
Square.h & Square.cpp descendant of Shape
ShapeList.h & ShapeList.cpp list of geometric shapes (provided)
oop10in.txt sample data

An Extensible List Class

First, we need a definition of the ShapeList class that represents a list of dynamically-allocated shapes. You can find the updated definition in the “Starter Code” or “Resources” section of this assignment. ShapeList now provides the following public interface:

public:
    ShapeList( ) = default;
    void add( Shape* newShape );
    void write( ostream& strm_out ) const;

NOTE: If you are starting this one early, you can use your own ShapeList definition from the previous lab assignment, provided that the add() and write() methods work correctly. Replace yours with the one provided when the lab assignment opens.

Shapes

Now, you will want to copy your definitions for the Shape and derived classes Circle and Square (and related functions/methods) into this project. Their nominal interfaces are shown below. You may have additional methods in your classes from the previous lab; that is OK, do not remove them. If the functionality you have is in conflict with any instructions below, you can simply modify your code slightly to remove the conflict.

// Class Shape should be placed in Shape.h
class Shape {
public:
    Shape( double x, double y );
    void write(std::ostream& strm_out) const;

private:
    double ref_x;
    double ref_y;
};
std::ostream& operator<< (std::ostream& strm_out, const Shape& s);

// Class Circle should be placed in Circle.h
class Circle : public Shape {
public:
    Circle( double x, double y, double r );
    void write(std::ostream& strm_out) const;

private:
    double radius;
};
std::ostream& operator<< (std::ostream& strm_out, const Circle& s);

// Class Square should be placed in Square.h
class Square : public Shape {
public:
    Square( double x, double y, double s );
    void write(std::ostream& strm_out) const;

private:
    double side;
};
std::ostream& operator<< (std::ostream& strm_out, const Square& s);

Polygon

Now you will add a new kind of Shape to your library. Create a class Polygon that should be a derived class of Shape. A Polygon is a Shape that has an array of (three or more) vertices defining the shape. Each of the vertices will be a Cartesian {\textstyle (x,y)} coordinate pair. To help with direct support for these 2-dimensional vertices, add the following helper class in Point2D.h & Point2D.cpp:

/**
 * Point2D is a 2-dimensional Cartesian
 * point with Real-valued coordinates.
 */
class Point2D {
public:
    double x;  ///< x-coordinate of this point
    double y;  ///< y-coordinate of this point

    /**
     * construct a Point2D given the x- and y-
     * coordinates of its location.
     */
    Point2D( double x, double y ) : x{x}, y{y} {}
    /**
     * default-construct a Point2D at the origin.
     */
    Point2D( ) : x( 0 ), y( 0 ) {}
};

std::ostream& operator<<( std::ostream& strm, const Point2D& point );
/**
 * Overloaded stream insertion operator to allow a Point2D to
 * be displayed in a standard ostream.
 * @param strm      the stream to write the Point2D into
 * @param point     the Point2D object to output
 * @return          the modified `strm` is returned
 */
std::ostream& operator<<( std::ostream& strm, const Point2D& point ) {
    return strm << "(" << point.x << ", " << point.y << ")";
}

Your Polygon’s constructor should take an array of Point2D objects, along with the size of that array, and should dynamically allocate a corresponding array internally and copy those points into it. The first point in the array should be used for the reference {\textstyle x}- and {\textstyle y}-coordinate for the Polygon. The Polygon’s destructor will then be responsible for freeing the memory associated with the array of vertices. A reference prototype for a Polygon is shown below:

class Polygon : public Shape {
public:
    Polygon( const Point2D* vertices, int vertex_count );
    ~Polygon( );
    void write( ostream& strm_out ) const;
    
    Polygon( const Polygon& )            = delete; // disallow copies by ctor
    Polygon& operator=( const Polygon& ) = delete; // and/or by assignment

private:
    Point2D* vertices     = nullptr;
    int      vertex_count = 0;
};

std::ostream& operator<<( std::ostream& strm_out, const Polygon& p );

Write the code to implement your Polygon class now. Be sure to copy the vertices constructor parameter correctly into the attribute vertices; be sure to implement the destructor so that no memory is leaked.

NOTE: You will need to explicitly sink the {\textstyle x}- and {\textstyle y}-coordinates from the first vertex to the base class constructor (Shape()) in a constructor initialization list. You cannot do this from within the body of the Polygon constructor, since the base class must be fully constructed before the derived class constructor (body) begins execution. (See the Point2D constructors for examples of the syntax of a constructor initialization list.) Remember, the order matters here: you must place the base class construction before any attributes, and then attributes can be constructed in the order they are declared.


Labcheckpoint.png FOR IN-LAB CREDIT: Show your progress so far, and explain the constructor and destructor for Polygon to the lab Instructor.


Reading and Writing Shape Data

Add write() methods with identical prototypes to each of the Shape object descendants. Also, add a write() method to the Shape implementation in order to provide default behavior; for example, the reference location can be written. This method will be called should a new descendant of Shape be defined without including its own write() method. The write() methods of each descendant Shape should also invoke Shape::write() to display their respective reference locations.

In order to test the execution of the various write() methods, create a data file oop10in.txt with the following contents.

Square   50 50  20;
Circle   50 50  10;
Square   50 50  9;
Polygon  40 40  50 60  70 30;
Circle   50 50  20;
Circle   60 38  1;
Polygon  60 60  70 80  90 70  100 60  80 50;

Diagram the objects indicated by the file.


Labcheckpoint.png FOR IN-LAB CREDIT: Show your progress so far, and explain the diagram of the objects above to the lab instructor.


Use the following as the body of function main() to test all methods to this point.

ifstream fin( "oop10in.txt" );
if ( !fin ) {
    cout << "Input file could not be opened!  Exiting!\n";
    exit( 1 );
}

ShapeList shapes;

while ( !fin.eof() ) {
    string object_type;
    fin >> object_type;
    if ( object_type == "Square" ) {

        // TODO: add a Square object to the list from the file

    } 
    else if ( object_type == "Circle" ) {

        // TODO: add a Circle object to the list from the file

    } 
    else if ( object_type == "Polygon" ) {

        // TODO: add a Polygon object to the list from the file

    } 
    else if ( object_type != "" ) {
        cout << "Unexpected object type: " << object_type << endl;
        exit( 2 );
    }
}

cout << "Object locations:\n" << shapes << endl;

Hint: Reading a Polygon

The Polygon specification in the data file looks like the following:

Polygon  x0 y0  x1 y1  x2 y2  x3 y3  [...]  xN yN;

You have no way of knowing how many pairs of {\textstyle x}- and {\textstyle y}-coordinate pairs to expect for any individual Polygon (although perhaps for this exercise an upper limit of 100 would be reasonable). You do know that the numbers will always come in pairs, and that the next item in the file following the last {\textstyle y}-coordinate (yN above) will be a semicolon character (‘;’) (or potentially the end-of-file).

These facts lend themselves well to either of two general approaches, described below:

Approach 1: Raw Stream Extraction

You can take advantage of the fact that the stream extraction operation will fail without removing any tokens from the stream when it encounters data that doesn’t match the expected type. So when the stream extraction operator expects to read the next {\textstyle x}-coordinate but encounters a non-digit (or the end-of-file), it will fail without removing anything more from the stream. With this in mind, your algorithm for reading the vertices of the polygon from the file might look like:

- create an array of points large enough to store any possible 
  Polygon (or be prepared to re-size an array dynamically)
- set up storage for a counter to count the number of vertices read, 
  as well as the x- and y-coordinates of the next vertex
- in a loop, attempt to extract the next x- and y-coordinate pair
  * as long as this extraction _succeeds_ (doesn't return a `false`-equivalent value):
    - set the next vertex in the array to the coordinates read
    - increment the count of the total number of vertices
* once the attempt to read the coordinate pair fails:
- create the new Polygon and place it into the shape list (you now have all of the
  vertices and the count).
- if the stream _did not_ fail because of encountering the end-of-file:
    - reset the stream's state by `clear()`-ing it so that the next
      read (which will be a Shape name) can succeed.

Approach 2: One Line at a Time with Help from istringstream

It would be very easy to read the entire line (containing the shape name and all of its coordinate pairs) into a std::string using getline(). Then you have a simpler problem to solve: How do you extract all of the values from this string? The C++ standard library provides an elegant solution for this: std::istringstream

A std::istringstream object (defined in the <sstream> library) is able to behave like an input stream, but can be initialized from a std::string. Consider the following simple example (that is not part of the solution you are trying to write, but is very related):

std::string        example{"picard 4 7 alpha tango"};
std::istringstream iss{example};
std::string        who;
int                num1, num2;
std::string        word1, word2;
iss >> who >> num1 >> num2 >> word1 >> word2;
if ( num1 == 4 && num2 == 7 && word1 == "alpha" && word2 == "tango" ) {
    who[0] = toupper( who[0] );
    std::cout << who << " has activated self destruct!\n";
}

This example will print “Picard has activated self destruct!”. For more information about std::istringstream (and its opposite, std::ostringstream), see http://www.cplusplus.com/reference/sstream/.

Execute the program and note that the output only shows the information provided by the Shape::write() method, not the individual methods defined in the derived classes.

Virtual Methods

Even with a Shape* pointer, a problem remains: method ShapeList::write() invokes Shape::write() for each object, but Circle::write(), Square::write(), etc. have access to more specific data concerning each object. Method Shape::write() can only display the reference location of the object, since the linker uses static binding for these methods. Virtual methods address this problem; change the prototype for Shape::write() to the following:

virtual void write (ostream& outfile) const;

A call to this method via a Shape* pointer will be automatically referred to the write() method of the descendant class, should there be one (e.g., Circle or Square); otherwise, Shape’s write() method is executed. This is the essence of polymorphism. The linker now uses dynamic binding, meaning that the decision on which write() method to invoke is delayed until run-time, and is based on the inheritance relationships of the involved objects at that time.

Add the virtual keyword to all of the write() method prototypes/headers, re-test your program to verify that the appropriate write() methods for each Shape are being used. Note: Adding the virtual keyword in the derived classes was not strictly necessary, but is considered a best practice.

Virtual Destructors

Inheritance introduces a destructor-based problem which must be addressed; some background must be covered to explain and then resolve it. When inheritance is involved, destructors are called in the reverse order that the corresponding constructors were called. This follows from the observation that any descendant method knows about its ancestors’ methods; it can access them even during destruction, so the ancestral portion of the object must not be destroyed before that of a descendant. Add the following one-line destructor to class Shape:

~Shape(){
    cout << "Shape::~Shape\n";
}

Using this format, add destructors to the following classes: Circle, Square, Polygon and ShapeList. Execute the program again and note that the messages are written for ShapeList and Shape only, in that order. The first message, that for ShapeList, occurs because the automatic variable of type ShapeList goes out of scope as function main() ends. The Shape destructors are called because the ShapeList destructor deletes each of the items in the list. The destructors for the descendants of Shape are not being called, because the ShapeList is only aware of the concept of a Shape, and the list items are being maintained as pointers to Shapes. The solution to this problem is to make ancestral destructors virtual. Modify the Shape destructor prototype as follows.

virtual ~Shape ( );

Execute the program again to see that the descendant shapes are now being destroyed. This includes the Polygon, which was actually managing dynamic resources that would be leaked if it were not destroyed properly.


Labcheckpoint.png FOR IN-LAB CREDIT: Demonstrate the correct output from all of the write() methods and verify for the lab instructor that the written order of destructor calls is descendant-to-ancestor.



Labsubmitsinglefile.png FOR IN-LAB CREDIT: Zip up these files: All files required to build your project and run the program.
Name the file oop10iL.zip and upload to CSCADE.