0

The title sucks I know. The problem here is that I have a 2D and 3D shape class that inherits from a shape class

class Shape {
    public:
    virtual double area() = 0;//Calculates the area of some kind of shape
    ~Shape() { delete this; }
};

class Shape2D :public Shape {
public:
    virtual double perimeter() = 0;//Calculates the perimeter of some kind of 2D-shape
    ~Shape2D() {};
};

class Shape3D :public Shape {
public:
    virtual double volume() = 0;//Calculates the volume of some kind of 3D-shape, area function will return surface area
    ~Shape3D() {};
};

It is decided that all shapes will default have an area. In 2D shapes, it'll have a virtual perimeter method and also and area from Shape. In 3D shapes, it'll have a volume and the vitual area method will be treated as a surface area.

The way that I've gone about is that within a menu that can choose 2d or 3d shapes: within the 2d menu, I initiate:

Shape2D * s = nullptr;

and within the 3d menu, I'll initiate:

Shape3D * s = nullptr;

and then to display whatever information, I use the methods:

void displayShape2D(Shape2D *)

and

void displayShape3D(Shape3D *)

However, the way that I want to go about it is to declare:

Shape *s = nullputr;

at the beginning of main and then at whatever shape the user chooses, I can just set:

s= new Triangle()

or

s = new cube();

The initialization works but when I try to make a display method, that's where I run into a problem. I want to be able to do:

displayShape(Shape *s)

When given a 2d shape and within the method I try:

cout <<s->perimeter();

It'll say that the perimeter member is not within the shape class. The problem then is trying to be able to determine if a the shape is 2d or 3d and then display the area and perimeter for 2d or surface area and volume for 3d. Is this possible or is creating the shape types within the dedicated menu and then having dedicated display methods the only way out?

000
  • 13
  • 3

2 Answers2

1

While the other answer "works" it's not the approach I'd take here. Basically you want to execute different code depending on the dynamic type of an instance: that's what virtual functions are for.

So just add a (possibly pure) virtual display member function to your Shape class and implement it accordingly in the deriving classes.

Contrary to the dynamic_cast approach this doesn't break when you add more deriving classes or classes that are derived even "further".

Finally:

~Shape() {
  delete this;
}

This destructor is the C++ equivalent of taking a shot gun and shooting yourself into the face. With a stack or static allocated instance this will lead to a bogus free (as the instance was never allocated from the heap), with a heap allocated it will lead to a double free (since the destructor is called just before freeing the occupied memory of the instance).

What you must do in your case is make the destructor virtual. Otherwise, having only Shape * it's impossible to correctly destruct the pointed to instance!


This is how such "display" functionality is "commonly" implemented, at least AFAIK:

struct Shape {
  virtual void write_to(ostream &) const = 0;
  virtual ~Shape() {}
};
struct Shape2D : public Shape {
  void write_to(ostream & stream) const {
    stream << "<2D, whooo>";
  }
};
struct Shape3D : public Shape {
  void write_to(ostream & stream) const {
    stream << "<I got depth, man!>";
  }
};

ostream & operator<<(ostream & stream, Shape const & shape) {
  shape.write_to(stream);
  return stream;
}

Now one can write any Shape (when using a pointer, dereference it) to any ostream, C++ "style":

std::unique_ptr<Shape> awesomeShape = std::make_unique<Shape2D>();
std::cout << "Awesome is: " << *awesomeShape << std::endl;

Here, first operator<<(ostream &, Shape &) is called (which it would for any Shape-like thing) which calls the virtual member function write_to, which is implemented in different ways for each derived class (though there could be a generic implementation in Shape, too!). See also this.


A possible issue with the dynamic_cast approach occurs when you deepen your hierarchy:

struct ThickShape2D : public Shape2D {
  // whatever
};

An instance with dynamic type ThickShape2D can also be dynamic_cast to a Shape2D, thus you'd need to keep an careful eye on the order of these if clauses.

But, to quote Jarra McIntyre:

I think it is worth mentioning that the design trade offs between using virtual functions and the above approach (and any other approaches) are complex. The above approach allows runtime dispatch on multiple types, and simplifies things such as (going back to the above) having multiple draw functions.

I completely second that. There's the visitor pattern (and also it's acyclic variant), probably the command pattern and a a lot of other things one can start looking at if one needs further information. For an extensive discussion about the uses of (raw) RTTI see this question and its answers.


As a final note: I don't know exactly what you're trying to model, but consider that inheritance is most often not the best approach available. When possible, prefer composition over inheritance. Entity component systems are a nice thing to look at.

(The above paragraph contains 6 links, just so that noone misses something.)

Community
  • 1
  • 1
Daniel Jour
  • 15,896
  • 2
  • 36
  • 63
  • Guy who answered above here. That's also a valid approach :) I suggested the approach I did because it didn't require any changes to the OP's existing code (except for fixing that destructor). I think it is worth mentioning that the design trade offs between using virtual functions and the above approach (and any other approaches) are complex. The above approach allows runtime dispatch on multiple types, and simplifies things such as (going back to the above) having multiple draw functions. – Jarra McIntyre Mar 06 '16 at 04:48
  • @JarraMcIntyre Indeed, I expanded a bit on that. – Daniel Jour Mar 06 '16 at 07:51
0

What you want to do is runtime type dispatch. One method is to use RTTI. This allows you to do something like this:

Shape *s = ...;

if(Shape3D *s3d = dynamic_cast<Shape3D*>(s)) {
    // s3d is now a pointer to a 3d shape
} else if(Shape2D *s2d = dynamic_cast<Shape2D*>(s)) {
    // s2d is now a pointer to a 2d shape
} else {
    // s is neither a Shape2D or Shape3D
}

This works because dynamic_cast(s) evaluates to a nullptr if s cannot be cast to Type*. Therefore the if statement condition only evaluates to true if s can be cast to the specified type.

Jarra McIntyre
  • 1,265
  • 8
  • 13