Starting from:

$35

COMP5421- Assignment 4 Solved

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.

More products