3
\$\begingroup\$

I'm reading through the chapter on inheritance from C++ Primer 5th Edition. It asks

Organize the following type into an inheritance hierarchy:

(b) Geometric primitives (such as box, circle, sphere, cone)

Identify some of the likely virtual functions as well as public and protected members.

I was hoping this is a suitable arrangement for the organisation of the hierarchy. My line of thinking was:

  1. The most general property of any shape is its dimension. Thus the code could be factored in to having a base class at the top named Shape which would contain this piece of data.

  2. The necessary member functions will change depending on whether I have a 1D, 2D or 3D shape. 2D shapes will require an area and perimeter function, and 3D shapes a surfaceArea and volume function. The operations of these functions will vary depending on the shape. So define abstract base classes corresponding to each dimension with pure-virtual member functions.

  3. Derive classes from the dimension classes corresponding to the final geometric shapes.

My main concern is that maybe the Shape class isn't all that necessary if its only purpose is to hold dimension and the static member pi. A second concern is how to prevent creation of Shape objects, since it only represents a concept and nothing tangible. I'm guessing to do this I would need to define the destructor as pure virtual - although I have not reached this stage of reading.

Shape.h:

#ifndef SHAPE
#define SHAPE

#include <cmath> //sqrt needed

class Shape{
public:
    Shape() = default;
    Shape(unsigned d): dimensions(d) { }
    virtual ~Shape() = default;
private:
    unsigned dimensions = 0;
protected:
    static const double pi;

};





class DimensionOne : public Shape {
public:
    DimensionOne(): Shape(1) { }

    virtual double length() const = 0;
};


class DimensionTwo : public Shape {
public:
    DimensionTwo(): Shape(2) { }

    virtual double perimeter() const = 0;
    virtual double area() const = 0;
};

class DimensionThree : public Shape {
public:
    DimensionThree(): Shape(3) { }

    virtual double surfaceArea() const = 0;
    virtual double volume() const = 0;
};







//2D SHAPES

class Rectangle : public DimensionTwo {
public:
    Rectangle() = default;
    Rectangle(double a): DimensionTwo(), length(a), width(a) { }
    Rectangle(double x, double y): DimensionTwo(), length(x), width(y) { }

    double perimeter() const override { return 2*(length+width); }
    double area() const override { return length*width;}
private:
    double length = 0, width = 0;
};

class Circle : public DimensionTwo {
public:
    Circle() = default;
    Circle(double r): DimensionTwo(), radius(r) { }

    double perimeter() const override { return 2*pi*radius; }
    double area() const override { return pi*radius*radius;}
private:
    double radius = 0;
};




//3D SHAPES

class Box : public DimensionThree {
public:
    Box() = default;
    Box(double a): DimensionThree(), length(a), width(a), height(a) { }
    Box(double x, double y, double z): DimensionThree(), length(x), width(y), height(z) { }

    double surfaceArea() const override { return (2*length*height) + (4*width*height); }
    double volume() const override { return length*width*height;}
private:
    double length = 0, width = 0, height = 0;
};

class Sphere : public DimensionThree {
public:
    Sphere() = default;
    Sphere(double r): DimensionThree(), radius(r) { }

    double surfaceArea() const override { return (4*pi*radius*radius); }
    double volume() const override { return (4*pi*radius*radius*radius)/3;}
private:
    double radius = 0;
};

class Cone: public DimensionThree {
public:
    Cone() = default;
    Cone(double b, double h): DimensionThree(), base(b), height(h) { }

    double surfaceArea() const override { return pi*base*(base + sqrt(base*base + height*height)); }
    double volume() const override { return (pi*base*base*height)/3;}
private:
    double base = 0, height = 0;
};


#endif // SHAPE

Shape.cpp:

#include "Shape.h"

const double Shape::pi = 3.14159;
\$\endgroup\$
  • \$\begingroup\$ Well done. But one of the reasons for having a common base class is that is the interface for common features. Why not add a draw() method to Shape so that it has at least one common interface. \$\endgroup\$ – Martin York Oct 11 '15 at 19:11
  • \$\begingroup\$ Also you have described the shapes well. But you have not described them in terms of position. So it is hard for them to interact (maybe thats another interface they need to inherit from). But if they had a location you could do intersections unions etc. \$\endgroup\$ – Martin York Oct 11 '15 at 19:14
1
\$\begingroup\$

A second concern is how to prevent creation of Shape objects, since it only represents a concept and nothing tangible. I'm guessing to do this I would need to define the destructor as pure virtual - although I have not reached this stage of reading.

Yep:

class Shape{
public:
    virtual ~Shape() = 0;
};
Shape::~Shape() {}

Now you can't create objects of Shape.

\$\endgroup\$
1
\$\begingroup\$

It may be easier to read and maintain this by:

  1. Adding a space between each operator instead of cramming them against the operands
  2. Having the class definitions in separate .cpp files

Also, if you're going to have such a small Shapes.cpp file, you might as well just provide the value of pi within the class declaration. I don't think the base class would need further implementation to warrant a separate .cpp file.

\$\endgroup\$
  • \$\begingroup\$ Would that not cause issues of multiple definitions of pi if I included Shape.h in multiple .cpp files? \$\endgroup\$ – AntiElephant Oct 11 '15 at 19:46
  • \$\begingroup\$ @AntiElephant: No, because it would only be defined once in the header and nowhere else. \$\endgroup\$ – Jamal Oct 11 '15 at 19:47
0
\$\begingroup\$

1.The most general property of any shape is its dimension. Thus the code could be factored in to having a base class at the top named Shape which would contain this piece of data.

Well, as every shape has its dimensionality fixed in the class definition, that info should not be a data member, but provided by a member function:

struct ShapeBase {
    virtual ~Shape() = default;
    virtual unsigned get_dimensions() const = 0; // pure virtual, must be overridden
};

That also takes care of your second concern. And the destructor is still trivial, which is a plus.

Also, there's no reason for giving the base shapes of each dimension different names. Use templates and specialize explicitly:

template <unsigned dim> struct Shape<dim>;

template<> struct Shape<1> : ShapeBase {
    unsigned get_dimensions() const final { return 1; }
    // marked final to forbid overriding further
};
\$\endgroup\$
  • \$\begingroup\$ Thanks for the reply. I heeded your first suggestion. As for templates, I will be learning about templates in due time so I'll come back to it when I know a bit more. \$\endgroup\$ – AntiElephant Oct 11 '15 at 19:47
0
\$\begingroup\$

Bug

Your formula for the surface area of a Box is wrong:

double surfaceArea() const override { return (2*length*height) + (4*width*height); }

It should be:

double surfaceArea() const override { return 2*(length*height + width*height +
                                                length*width); }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.