$35
Purpose
• Practice fundamental object-oriented programming (OOP) concepts
• Implement an inheritance hierarchy of classes C++
• Learn about virtual functions, overriding, and polymorphism in C++
• Use two-dimensional arrays using array and vector, the two simplest container class templates in the C++ Standard Template Library (STL)
• Use modern C++ smart pointers, avoiding calls to the delete operator for good!
2 Overview
Using simple two-dimensional geometric shapes, this assignment will give you practice with the fundamental principles of object-oriented programming (OOP).
The assignment starts by abstracting the essential attributes and operations common to the geometric shapes of interest in this assignment, namely, rhombus, rectangle, and two kinds of triangle shapes.
You will then be tasked to implement the shape abstractions using the C++ features that support encapsulation, information hiding, inheritance and polymorphism.
In addition to implementing the shape classes, you will be tasked to implement a Canvas class whose objects can be used by the shape objects to draw on.
The geometric shapes of interest in this assignment are four simple two-dimensional shapes which can be textually rendered into visually identifiable images on the computer screen; specifically: rhombus , rectangle, and two special kinds of isosceles triangles.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
Rectangle,
6 × 9
Rhombus,
5 × 5
Right Triangle,
6 × 6
Acute Triangle,
5 × 9
Here are examples of the specific shapes of interest:
3 Modeling 2D Geometric Shapes
3.1 Common Attributes: Data
The 2D shapes of interest to us have five attributes in common. Specifically, each shape has a name, a string object; for example, “Book” for a rectangular shape an identity number, a unique positive integer, distinct from that of all the other shapes a pen character, the single character to use when drawing the shape a height, a non-negative integer a width, a non-negative integer
Note Here, we assume that the height and width of a shape measure, respectively, that shape’s vertical and horizontal attributes, although they may be called by a different name for different shapes; for example, both attributes for a rhombus are called
“diagonal”, and the horizontal attribute of a triangle is called “base.”
3.2 Common Operations: Interface
Listed below are the services that every concrete 2D geometric object is expected to provide.
3.2.1 General Operations
1. A constructor that accepts as parameters the initial values of a shape’s height, width, pen, and name, in that order.
2. Five accessor (getter) methods, one for each attribute;
3. Two mutator (setter) methods for setting the name and pen members;
4. A toString() method that forms and returns a string representation of the this shape
3.2.2 Shape-Specific Operations
5. Two mutator (setter) methods for setting the height and width members;
6. A method to compute and return the shape’s geometric area;
7. A method to compute and return the shape’s geometric perimeter;
8. A method to compute the shape’s textual area, which is the number of characters that form the textual image of the shape;
9. A method to compute the shape’s textual perimeter, which is the number of characters on the borders of the textual image of the shape;
10. A method that draws a textual image of the shape on a Canvas object using the shape’s pen character.
4 Modeling Specialized 2D Geometric Shapes
There are several ways to classify 2D shapes, but we use the following, which is specifically designed for you to gain experience with implementing inheritance and polymorphism in C++:
Figure 1: A UML class diagram showing an inheritance hierarchy specified by two abstract classes Shape and Triangle , and by four concrete classes Rectangle, Rhombus, AcuteTriangle, and RightTriangle.
Encapsulating the attributes and operations common to all shapes, class Shape must necessarily be abstract[1] because the shapes it models are so general that it simply would not know how to implement the operations specified in section 3.2.2.
As a base class, Shape serves as a common interface to all classes in the inheritance hierarchy.
As an abstract class, Shape makes polymorphism in C++ possible through the types Shape* and Shape&.[2]
Similarly, class Triangle must be abstract, since it would have no knowledge about the specific triangular shapes it generalizes.
Classes Rectangle, Rhombus, RightTriangle and AcuteTriangle are concrete because they each fully implement their respective interface.
5 Concrete Shapes
The specific features of these concrete shapes are listed in the following table.
5.1 Shape Notes
The unit of length is a single character; thus, both the height and width of a shape are measured in characters.
At construction, a Rectangle shape requires the values of both its height and width, whereas the other three concrete shapes each require a single value for the length of their respective horizontal attribute.
6 Task 1 of 2
Implement the Shape inheritance class hierarchy described above. It is completely up to you to decide which operations should be virtual, pure virtual, or non-virtual, provided that it satisfies a few simple requirements.
The amount of coding required for this task is not a lot as your shape classes will be small. Be sure that common behavior (shared operations) and common attributes (shared data) are pushed toward the top of your class hierarchy; for example:
6.1 Modeling 2D Triangle Shapes
6.1.1 Common Attributes
The common attributes of triangles are their heights and bases which are already being represented by height and width data members in Shape.
6.1.2 Common Operations
A method to return a geometric area of the triangle
Note: Without knowledge about its shape, a Triangle object can compute its area based on its height and width.
6.1.3 Type-Specific Operations
A method to return a triangle’s height which may depend on is base
A method to return a triangle’s width which may depend on is height
A method to return a geometric perimeter of the triangle
Note: Without knowledge about its shape, a Triangle object is unable to compute its perimeter based on its height and width.
7 Some Examples
The call rect.toString() on line 2 of the source code generates the entire output shown. However, note that line 4 would produce the same output, as the output operator overload itself internally calls toString().
Line 3 of the output shows that rect’s ID number is 1. The ID number of the next shape will be 2, the one after 3, and so on. These unique ID numbers are generated and assigned when shape objects are first constructed.
Lines 4-5 of the output show object rect’s name and pen character, and lines 6-7 show rect’s width and height, respectively.
Now let’s see how rect’s static and dynamic types are produced on lines 12-13 of the output.
To get the name of the static type of a pointer p at runtime you use typeid(p).name(), and to get its dynamic type you use typeid(*p).name(). That’s exactly what toString() does at line 2, using this instead of p. You need to include the <typeinfo> header for this.
As you can see on lines 12-13, rect’s static type name is PK5Shape and it’s dynamic type name is 9Rectangle. The actual names returned by these calls are implementation defined. For example, the output above was generated under g++ (GCC) 10.2.0, where PK in PK5Shape means “pointer to konstconst”, and 5 in 5Shape means that the name “Shape” that follows it is 5 character long.
Microsoft VC++ produces a more readable output as shown below.
Shape Information -----------------
id: 1
Shape name: Rectangle
Pen character: *
Height: 5
Width: 7
Textual area: 35
Geometric area: 35.00
Textual perimeter: 20
Geometric perimeter: 24.00
Static type: class Shape const *
Dynamic type: class Rectangle
Rectangle rect{ 5, 7 }; cout << rect.toString() << endl;
// or equivalently
// cout << rect << endl;
1
2
3
4
5
1
6
2
7
3
8
4
9
10
11
12
13
Here is an example of a Rhombus object:
Shape Information -----------------
id: 2
Shape name: Ace of diamond
Pen character: v
Height: 17
Width: 17
Textual area: 145
Geometric area: 144.50
Textual perimeter: 32
Geometric perimeter: 48.08
Static type: class Shape const *
Dynamic type: class Rhombus
Rhombus ace{16, ’v’, "Ace of diamond"};
// cout << ace.toString() << endl; // or, equivalently: cout << ace << endl;
14
15
16
17
518
619
720
821
922
23
24
25
26
Notice that in line 6, the supplied height 16 is invalid because it is even; to correct it, Rhombus’s constructor uses the next odd integer, 17, as the diagonal of object ace.
Again, lines 7 and 9 would produce the same output; the difference is that the call to toString() is implicit in line 9.
Here are examples of AcuteTriangle and RightTriangle shape objects.
Shape Information -----------------
id: 3
Shape name: Wedge
Pen character: *
Height: 9
Width: 17
Textual area: 81
Geometric area: 76.50
Textual perimeter: 32
Geometric perimeter: 41.76
Static type: class Shape const *
Dynamic type: class AcuteTriangle
AcuteTriangle at{ 17 }; cout << at << endl;
/*equivalently:
Shape *atptr = &at; cout << *atptr << endl;
Shape &atref = at; cout << atref << endl;
*/
27
10
28
11
29
1230
1331
1432
1533
1634
1735
36
18
37
19
38
20
39
Shape Information -----------------
id: 4
Shape name: Carpenter’s square
Pen character: L
Height: 10
Width: 10
Textual area: 55
Geometric area: 50.00
Textual perimeter: 27
Geometric perimeter: 34.14
Static type: class Shape const *
Dynamic type: class RightTriangle
RightTriangle rt{ 10, ’L’, "Carpenter’s
cout << rt << endl;
// or equivalently
// cout << rt.toString() << endl;
40
41
42
43
2144
22 square" 45};
2346
2447
2548
49
50
51
52
7.1 Polymorphic Magic
Note that on line 22 in the source code above, rt is a regular object variable, as opposed to a pointer (or reference) variable pointing to (or referencing) an object; as such, rt cannot make polymorphic calls. That’s because in C++ the calls made by a regular object, such as rect,ace, at, and rt, to any function (virtual or not) are bound at compile time (early binding).
Polymorphic magic happens through the second argument in the calls to the output operator<< at lines 4, 9, 11, and 23. For example, consider the call cout << rt on line 23 which is equivalent to operator<<(cout, rt). The second argument in the call, rt, corresponds to the second parameter of the output operator overload:
ostream& operator<< (ostream& out, const Shape& shp);
Specifically, rt in line 23 gets bound to parameter shp which is a reference and as such, can call virtual functions of Shape polymorphically. That means, the decision as to which function to invoke depends on the type of the object referenced by shp at run time (late binding).
For example, if shp references a Rhombus object, then the call shp.geoArea() binds to Rhombus::geoArea(), if shp references a Rectangle object, then shp.geoArea() binds to Rectangle::geoArea(), and so on.
However, consider rt on line 25; although rt is not a reference or a pointer, it is the invoking object in the call rt.toString() which is represented inside Shape::toString() by the this pointer, which in fact can call virtual functions of Shape (the base class) polymorphically.
7.2 Shape’s Draw Function
virtual Canvas draw() const = 0; // concrete derived classes must implement it
Introduced in Shape as a pure member function, the draw() function forces concrete derived classes to implement it.
Defining a Canvas object like so
Canvas can { getHeight(), getWidth() };
the draw function draws on can using its put members function, something like this:
can.put(r, c, penChar); // write penChar in cell at row r and column c
0 1 2 3 4
A Canvas object models a two-dimensional grid as abstracted in the Figure 0 at right. The rows of the grid are parallel to the x-axis, with row numbers 1 increasing down. The columns of the grid are parallel to the y-axis, with 2 column numbers increasing to the right. The origin of the grid is located at the top-left grid cell (0,0) at row 0 and column 0.
7.3 Examples Continued
v
vvv vvvvv vvvvvvv vvvvvvvvv vvvvvvvvvvv
vvvvvvvvvvvvv vvvvvvvvvvvvvvv vvvvvvvvvvvvvvvvv
vvvvvvvvvvvvvvv
vvvvvvvvvvvvv
vvvvvvvvvvv
vvvvvvvvv
vvvvvvv
vvvvv
vvv v
Canvas aceCan = ace.draw(); cout << aceCan << endl;
53
54
55
56
57
58
59
2660
2761
2862
63
64
65
66
67
68
69
*******
*******
*******
*******
*******
Canvas rectCan = rect.draw(); cout << rectCan << endl;
70
2971
3072
3173
74
^
^^^
^^^^^
^^^^^^^
^^^^^^^^^
^^^^^^^^^^^
^^^^^^^^^^^^^
^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^
at.setPen(’^’);
Canvas atCan = at.draw(); cout << atCan << endl;
75
76
77
32
78
33
79
34
80
35
81
82
83
L
LL
LLL
LLLL
LLLLL
LLLLLL
LLLLLLL
LLLLLLLL
LLLLLLLLL
LLLLLLLLLL
Canvas rtCan = rt.draw(); cout << rtCan << endl;
84
85
86
87
36
88
37
89
38
90
91
92
93
A Canvas object can be flipped both vertically and horizontally:
O
OO
OOO
OOOO
OOOOO
OOOOOO
OOOOOOO
OOOOOOOO
OOOOOOOOO
OOOOOOOOOO
rt.setPen(’O’);
Canvas rtQuadrant_1 = rt.draw(); cout << rtQuadrant_1 << endl;
94
95
96
3997
4098
4199
42100
101
102
103
O
OO OOO OOOO OOOOO
OOOOOO
OOOOOOO OOOOOOOO OOOOOOOOO
OOOOOOOOOO
cout << rtQuadrant_2 << endl;
104
105
106
107
43 108
44 Canvas rtQuadrant_2 = rtQuadrant_1.flip_horizontal();
109
45
110
111
112
113
OOOOOOOOOO
OOOOOOOOO
OOOOOOOO
OOOOOOO
OOOOOO
OOOOO
OOOO
OOO
OO O
cout << rtQuadrant_3 << endl;
114
115
116
117
46 118
47 Canvas rtQuadrant_3 = rtQuadrant_2.flip_vertical();
119
48
120
121
122
123
OOOOOOOOOO
OOOOOOOOO
OOOOOOOO
OOOOOOO
OOOOOO
OOOOO
OOOO
OOO
OO
O
cout << rtQuadrant_4 << endl;
124
125
126
127
49 128
50 Canvas rtQuadrant_4 = rtQuadrant_3.flip_horizontal();
129
51
130
131
132
133
Now, let’s create a polymorphic vector of shapes and draw them polymorphically:
*******
*******
*******
*******
*******
v
vvv vvvvv vvvvvvv vvvvvvvvv
vvvvvvvvvvv vvvvvvvvvvvvv vvvvvvvvvvvvvvv vvvvvvvvvvvvvvvvv
vvvvvvvvvvvvvvv
vvvvvvvvvvvvv
vvvvvvvvvvv
vvvvvvvvv
vvvvvvv
vvvvv
vvv v
*
***
*****
*******
*********
***********
*************
***************
*****************
L
LL
LLL
LLLL
LLLLL
LLLLLL
LLLLLLL
LLLLLLLL
LLLLLLLLL
LLLLLLLLLL
// first, create a polymorphic // vector<smart pointer to Shape>: std::vector<std::unique_ptr<Shape>> shapeVec;
// Next, add some shapes to shapeVec shapeVec.push_back
(std::make_unique<Rectangle>(5, 7)); shapeVec.push_back
(std::make_unique<Rhombus>(16, ’v’, "Ace")); shapeVec.push_back
(std::make_unique<AcuteTriangle>(17)); shapeVec.push_back
(std::make_unique<RightTriangle>(10, ’L’));
// now, draw the shapes in shapeVec for (const auto& shp : shapeVec) cout << shp->draw() << endl;
134
135
136
137
138
139
140
141
142
143
144
145
146
52
147 53
148
54
149
55
150
56
151
57
152
58153
59154
60155
61156
62157
63158
159
64
160
65
161
66
162
67
163
68
164
69
165
166
167
168
169
170
171
172
173
174
175
176
177
8 Task 2 of 2
Implement a Canvas class using the following declaration. Feel free to introduce other private member functions of your choice to facilitate the operations of the other members of the class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Canvas
{ public:
// all special members are defaulted because ’grid’,
// the only data member, is self-sufficient and efficient; that is,
// it is equipped to handle the corresponding operations efficiently
Canvas() = default; virtual ~Canvas() = default; Canvas(const Canvas&) = default; Canvas(Canvas&&) = default;
Canvas& operator=(const Canvas&) = default;
Canvas& operator=(Canvas&&) = default;
protected:
vector<vector<char> > grid{}; // the only data member bool check(int r, int c)const; // validates row r and column c
void resize(size_t rows, size_t cols); // resizes this Canvas’s dimensions
public:
// creates this canvas’s (rows x columns) grid filled with blank characters Canvas(int rows, int columns, char fillChar = ’ ’);
int getRows()const; // returns height int getColumns()const; // returns width
Canvas flip_horizontal()const; // flips this canvas horizontally Canvas flip_vertical()const; // flips this canvas vertically void print(ostream&) const; // prints to ostream
char get(int r, int c) const; // returns char at row r and column c void put(int r, int c, char ch); // puts ch at row r and column c; this is
// only function used by a shape’s draw
// returns doing nothing if r or c is invalid
// draws text starting at row r and col c on the canvas void drawString(int r, int c, const std::string text);
// copies the non-blank characters of "can" onto the invoking canvas; // maps can’s origin to row r and column c on the invoking canvas void overlap(const Canvas& can, size_t r, size_t c);
}; ostream& operator<< (ostream& sout, const Canvas& f);
29 the
30 functon;
31
32
33
34
35
36
37
38
39
40
8.1 FYI
To make the assignment workload lighter, the following features were dropped from the original version of Canvas. They are listed here so that you might want to implement them some time after the exam to enhance your Canvas class.
Allow the user to index both rows and column from 1
Overload the function call operator as a function of two size t arguments to write on a canvas, similar to put. For example:
char ch {’*’}; can(1, 2) = ch;
// similar to can.put(1, 2, ch);
ch = can(1, 2);
// similar to ch = can.get(1,2)
To serve both const and non-const objects of Canvas, provide two version of the operator.
Overload the subscript operator to to support this code segment:
char ch {’*’}; can[1][2] = ch;
// similar to can.put(1, 2, ch);
ch = can[1][2];
// similar to ch = can.get(1,2)
To serve both const and non-const objects of Canvas, provide two version of the operator.
Overload the binary operator+ to join two Canvas objects horizontally. The returning Canvas object will be large enough to accommodate both Canvas objects.
[1] Recall that a C++ class is said to be abstract if it has at least one pure virtual function. You cannot define an object of an abstract class Foo, but you can define variables of types Foo* and Foo&. The compiler ensures that all calls to a virtual function (pure or not) via Foo* and Foo& are polymorphic calls.
Any class derived from an abstract class will itself be abstract unless it overrides all the pure virtual functions it inherits.
[2] A pointer (or reference) to an object with a virtual member function has two types: static and dynamic.
static type: refers to its type as defined in the source code and thus cannot change. For example, the static type of the pointer variable pf as in Foo* pf; is Foo*, a pointer to Foo, a type that cannot be changed, in the sense that pf will always remain a pointer to Foo.
dynamic type: refers to the type of the object the pointer points to (or references) at runtime and thus can change during runtime. For example, although pf points to (stores the address of) any shape in the inheritance hierarchy, it may point to different shapes during its lifespan.