Problem overview
A drawing app keeps different kinds of shapes in one list. You write the shape classes so that code holding that list can ask any shape for its name() and its perimeter() without knowing which kind it is.
The classes are:
Shape, the common parent: name"Shape", perimeter 0;Rectangle(width, height): perimeter2 * (width + height);Square(side): perimeter4 * side;Triangle(a, b, c): perimetera + b + c.
Then two methods of Solution work on a vector<Shape*>: totalPerimeter adds up all the perimeters, and largestShape returns the name of the shape with the largest perimeter, the first one if there is a tie.
In the first example the shapes are a 3 by 4 rectangle, a square of side 5 and a triangle with sides 3, 4 and 5. Their perimeters are 14, 20 and 12, so the total is 46 and the square is the largest.
Approach: ask the object, don't check its class
The list holds Shape* pointers, whatever the real kind of each shape. Because name and perimeter are virtual in Shape, a call such as shape->perimeter() runs the version that belongs to the object the pointer points to. A Square adds nothing up, it multiplies by four. A Triangle adds three sides. The loop that calls perimeter() doesn't need to know.
The statement asks for exactly this: both Solution methods use only the methods Shape has, and never test the kind of a shape. That keeps them working when someone adds a new shape later. For example, with a Hexagon class that inherits from Shape and overrides both methods, a list holding a 3 by 4 rectangle, a hexagon of side 4 and a square of side 5 gives 58 Hexagon, with neither method changed.
Step by step
Shape. Return"Shape"fromnameand 0 fromperimeter. The children override both, so these defaults only give the common type its two methods. The virtual destructor is already in the starter code.- The fields. Each shape keeps its sizes in
intfields:widthandheightfor the rectangle,sidefor the square,a,bandcfor the triangle. The perimeter methods need them later, so every object remembers its own. - The constructors. The parameters have the same names as the fields, so
this->width = width;means "store the parameter in the field". - The overrides. Each class returns its own name, with a capital letter, and the formula for its perimeter.
totalPerimeter. Start a total at 0, loop over the shapes with a range-basedforand addshape->perimeter()to it. Return the total after the loop.largestShape. Letlargeststart as the first shape,shapes[0], which always exists. Loop over all shapes. If one has a strictly greater perimeter thanlargest, it becomes the newlargest. After the loop, returnlargest->name().
The solution in C++
class Shape {
public:
virtual string name() {
return "Shape";
}
virtual int perimeter() {
return 0;
}
virtual ~Shape() = default;
};
class Rectangle : public Shape {
public:
int width;
int height;
Rectangle(int width, int height) {
this->width = width;
this->height = height;
}
string name() override {
return "Rectangle";
}
int perimeter() override {
return 2 * (width + height);
}
};
class Square : public Shape {
public:
int side;
Square(int side) {
this->side = side;
}
string name() override {
return "Square";
}
int perimeter() override {
return 4 * side;
}
};
class Triangle : public Shape {
public:
int a;
int b;
int c;
Triangle(int a, int b, int c) {
this->a = a;
this->b = b;
this->c = c;
}
string name() override {
return "Triangle";
}
int perimeter() override {
return a + b + c;
}
};
class Solution {
public:
int totalPerimeter(vector<Shape*>& shapes) {
int total = 0;
for (Shape* shape : shapes) {
total += shape->perimeter();
}
return total;
}
string largestShape(vector<Shape*>& shapes) {
Shape* largest = shapes[0];
for (Shape* shape : shapes) {
if (shape->perimeter() > largest->perimeter()) {
largest = shape;
}
}
return largest->name();
}
};
A run
Building the shapes of the three examples and calling both methods gives:
46 Square
36 Rectangle
15 Triangle
The second line is the tie case: a 2 by 4 rectangle, a square of side 3 and a triangle with sides 4, 4 and 4 all have perimeter 12, and the rectangle comes first. In the third, the last shape wins because its perimeter, 7, is larger than the 4 of the other two.
Details that matter
- Pointers use
->. AShape*reaches its methods with->, as inshape->perimeter(), not with a dot. - The names belong to the objects. A 1 by 1 rectangle has the same sides as a square, but it is still a
Rectangle: it printsRectangle, because the object answers for its own class. - A base pointer to a derived object. A
Shape*can point to aSquare, since a square is a shape. The reverse isn't true. - The virtual destructor in
Shapeletsdeleteon aShape*clean up the real object correctly.
Complexity
Each method visits every shape once and does a constant amount of work for each, so the time is O(n) for n shapes. Only a few variables are added, so the extra space is O(1).
Edge cases and common mistakes
- Using
>=inlargestShape. The last of several equal shapes would win. With the tie example above, the answer would beTriangleinstead ofRectangle. Only a strictly greater perimeter may replacelargest. - Forgetting
virtualon a method. Ifperimeteris not virtual inShape, a call through aShape*runs the parent's version. With that change, a square of side 5 reached through aShape*reports a perimeter of 0. - Shadowing in the constructor. Writing
width = width;withoutthis->assigns the parameter to itself and never sets the field, which keeps an indeterminate value. ForRectangle r(3, 4)the testr.width == 3came out false. - Checking the kind of the shape with
ifchains. It works for today's shapes, but every new shape would need a new branch. - Using
shapes[0]on an empty list. The tests never give an empty list, so the first shape is always there.
Testing
Three small lists to try. The first number is the total, then the name:
| Shapes | Result |
|---|---|
| Rectangle(3, 4), Square(5), Triangle(3, 4, 5) | 46 Square |
| Rectangle(2, 4), Square(3), Triangle(4, 4, 4) | 36 Rectangle |
| Square(1), Rectangle(1, 1), Triangle(2, 2, 3) | 15 Triangle |
