Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » C++ derived classes, grandchildren<-->grandparents

Credits go to anonymous, bamccaig, ReyBrujo, Schyfis, and Wetimer for helping out!
This thread is locked; no one can reply to it. rss feed Print
C++ derived classes, grandchildren<-->grandparents
OnlineCop
Member #7,919
October 2006
avatar

Our class is creating shapes in OpenGL. We are given the drawing routines, while we need to calculate the position of each of the polygons' points ourselves.

Since there are multiple shapes, I define a base class, Shape.

One shape is a Cube, with a 1:1:1 ratio of all its sides. I'm given the cube's diameter in a text file, and set its height, width, and depth to the same value (diameter / 2) and center it around a point.

Another shape is a Box, where height, width, and depth are defined differently.

Is a Box derived from Cube, or Cube from Box?

I would think "Cube is a Box with 1's as its height/width/depth". It's most logical to me.

But if I'm overriding the "read the dimensions in from a stream", I have a problem:

  1. Shape will read in the shape's color (for all polygons) and the coordinates where its center will be located (x, y, and z axis).

  2. Box will let Shape read in the above, and then it will read in the height, width, and depth it needs.

At this point, all is fine. But when I want to create Cube, I can't read in 3 points for its dimensions: I can only read in 1 (the file won't specify H,W, and D... just one of them).

If Cube is derived from Shape, I don't have problems, though I have a lot of duplicated code, which is what I want to avoid.

But if Cube is derived from Box (which is derived from Shape), both Shape and Box try to read in their streams before Cube "gets control" of the stream.

If Cube is declared like this:

class Cube
   : public Box
{
   // ...
}

...and I wanted to do something like this:

Cube::Cube(std::istream& is)
   : Shape(is)
{
   float diameter;
   is >> diameter;
   dimensions = Point(diameter, diameter, diameter);
}

...then I get a compile error:

Cube.cpp: In constructor 'Cube::Cube()':
Cube.cpp:10: error: type 'Shape' is not a direct base of 'Cube'

But again, if I change the constructor to:

Cube::Cube(std::istream& is)
   : Box(is)
{
   // ...
}

...then it tries to read in H, W, and D which is not what I want.

Any suggestions on how I can call the super class of the current object's super class?

Wetimer
Member #1,622
November 2001

I'm afraid you have to call a constructor on the immediate base class. Otherwise, a calls could not be sure that its constructor had actually been called which could cause somethings to break.

What you could do:

1. Have Box provide a default constructor which Cube calls.

2. Don't use the constructor for input purposes, instead, have other functions which extract the input and then invoke the classes constructor. Perhaps you don't need a cube class at all; instead, just construct a Box with all dimensions the same.

3. Have Box and Cube inherit from a class BoxBase which has has all the box methods but doesn't try to read in the input. The Box and Cube classes just change how they handle input.

<code>if(Windows.State = Crash) Computer.halt();</code>

OnlineCop
Member #7,919
October 2006
avatar

I have #1, so Shape can be called with 0 arguments, or with a stream (and therefore, its derived class Box has the same).

For #2, Shape's constructor itself doesn't read in the data from the file; it calls a readData() function which does it. Maybe I should have Cube call Shape's readData() function?

#3 actually sounds pretty good. I'll try this approach first, then work my way back. Thanks for suggesting it.

bamccaig
Member #7,536
July 2006
avatar

I'm thinking Cube should derive from Box, but the necessary loading operation for Box should be overridden in Cube... :-/ So the loading should be broken off into a virtual method that can be overridden in derived classes.

Disclaimer: I'm drunk... I apologize if that was already said...

Schyfis
Member #9,752
May 2008
avatar

Does anyone else see "//-->" above the allegro.cc logo on this page only?

________________________________________________________________________________________________________
[freedwill.us]
[unTied Games]

OnlineCop
Member #7,919
October 2006
avatar

I see it. Probably has to do with me having "<-->" in the title of the thread. It must have thought MY "-->" was its own "-->".

ReyBrujo
Moderator
January 2001
avatar

Hmm... I don't like having the constructor do anything other than initializing variables with default values, since the only way to handle errors there is with an exception.

I would create a virtual load method in Shape, then let the shapes handle it correctly. Maybe even making the class abstract to prevent anyone from instantiating it:

class Shape {
    public:
        virtual bool Load(std::istream& is) = 0;
};

class Box {
    public:
        virtual bool Load(std::istream &is) {
            // TODO: magic
        }
};

Since all shapes appear to load data from files, it would be useful to have the function there (unless the shapes are DTOs and you don't want methods other than the constructor in them).

--
RB
光子「あたしただ…奪う側に回ろうと思っただけよ」
Mitsuko's last words, Battle Royale

anonymous
Member #8025
November 2006

Quote:

I have #1, so Shape can be called with 0 arguments, or with a stream (and therefore, its derived class Box has the same).

This doesn't follow. Constructors are not inherited: all derived classes might have completely different signatures and have no use for some or all of the base class constructor signatures. (You might provide a protected default constructor of Cube, to be only used by the derived classes.)

The problem itself is well-known (Modelling Circle-Ellipse), although as I understand it won't be that bad if the instances are immutable. (This has not so much to do with constructors, but what you might run into modeling things like geometrical shapes as an inheritance hierarchy.)

However, if Box derives from Cube and adds 2 more lengths to it, I don't see why the current approach should be causing problems.

1#include <iostream>
2#include <sstream>
3#include <stdexcept>
4 
5struct shape_construction_error: public std::runtime_error
6{
7 shape_construction_error(const std::string& msg): std::runtime_error(msg) {}
8};
9 
10 
11class Shape
12{
13};
14 
15class Cube: public Shape
16{
17protected:
18 int a;
19public:
20 Cube(std::istream& is)
21 {
22 if (!(is >> a))
23 throw shape_construction_error("failed cube");
24 }
25 std::ostream& print(std::ostream& os) const
26 {
27 return os << '[' << a << ", " << a << ", " << a << "]\n";
28 }
29};
30 
31class Box: public Cube
32{
33protected:
34 int b, c;
35public:
36 Box(std::istream& is): Cube(is)
37 {
38 if (!(is >> b >> c))
39 throw shape_construction_error("failed box");
40 }
41 std::ostream& print(std::ostream& os) const
42 {
43 return os << '[' << a << ", " << b << ", " << c << "]\n";
44 }
45};
46 
47int main()
48{
49 std::stringstream a("5"), b("10 12 14");
50 Cube c1(a); c1.print(std::cout);
51 Box c2(b); c2.print(std::cout);
52}

If a cube fundamentally behaves the same as a box, except having all sides equal, you might just use the same class for it and, for example, allow creation of two types of boxes (cubes/boxes) using the "named constructor idiom".

1#include <iostream>
2#include <sstream>
3#include <stdexcept>
4 
5struct shape_construction_error: public std::runtime_error
6{
7 shape_construction_error(const std::string& msg): std::runtime_error(msg) {}
8};
9 
10class Shape
11{
12};
13 
14class Box: public Shape
15{
16protected:
17 int a, b, c;
18public:
19 std::ostream& print(std::ostream& os) const
20 {
21 return os << '[' << a << ", " << b << ", " << c << "]\n";
22 }
23 
24 //construction of two types of boxes only possible through these static methods
25 static Box make_cube(std::istream& is)
26 {
27 int a;
28 if (!(is >> a))
29 throw shape_construction_error("failed cube");
30 return Box(a);
31 }
32 
33 static Box make_box(std::istream& is)
34 {
35 int a, b, c;
36 if (!(is >> a >> b >> c))
37 throw shape_construction_error("failed box");
38 return Box(a, b, c);
39 }
40 
41private:
42 //cube constructor
43 Box(int a): a(a), b(a), c(a) {}
44 //box constructor
45 Box(int a, int b, int c): a(a), b(b), c(c) {}
46};
47 
48int main()
49{
50 std::stringstream a("5"), b("10 12 14");
51 Box c1 = Box::make_cube(a);
52 c1.print(std::cout);
53 Box c2 = Box::make_box(b);
54 c2.print(std::cout);
55}

(Whether you put objects into the final state in the constructor and throw exceptions if you can't do so, or you just create default instances and load them later - again throwing an exception or returning success/failure, seems a bit like a matter of taste (the load+error code method requiring somewhat more discipline from the programmer).

OnlineCop
Member #7,919
October 2006
avatar

anonymous, I mis-worded that. It's not that "since my constructor has ___, my derived classes required me to implement the same functionality." Instead, I was trying to say that I had implemented them all the same, just because each of my derived classes thus far have needed the same functionality.

I went with the approach of having a BasicBox object that derived from Shape, and then Cube and Box both derived from BasicBox. That way, BasicBox didn't actually draw any information from the file, though BasicBox's parent, Shape, did.

I essentially wanted to "skip a generation" when I was loading data. The Shape NEEDED to load the object's center X,Y,Z coordinates as well as the shape's color, and all Shape formats (current and planned) would start their data with the same structure. Having BasicBox was a good trade-off, though it seems a bit clunky if I will have more related types in the future, like a Sphere and and Ovoid.

Tobias Dammers
Member #2,604
August 2002
avatar

No need for dedicated Cube and Box classes, really. Just Box, and give it two ctors: one with a single size argument, one with 3 sizes. Everything else is pretty much the same, at least the difference doesn't really justify dedicated classes.
If, OTOH, you are required to use dedicated classes, I'd use a hierarchy something like:

Shape (abstract)
 +- BoxShape (abstract, introduces getW(), getH(), getD() or whatever as abstract methods, and implements the common functionality for box and cube that doesn't work for other shapes like spheres)
      +- Cube (overloads all 3 getXXX() methods to return the same member var's value)
      +- Box (overloads getXXX() to return the individual components)

The reason why I'm saying this is that conceptually, you can argue two ways - either a cube is a special case of a box; if you go this route, then you can use the general-case code for both. Or a box and a cube are two different things, both inheriting from an abstract common concept; in this case, the common abstract base class BoxShape represents the underlying concept, with Box and Cube as its (otherwise independent) manifestations.

More practically, if your Box has a member var "size", and you derive Cube from Box, then you can either re-use "size" as one of the dimensional sizes (which is not a good thing really - which of the dimensions is "the" size? they're all equivalent, and none is functionally the same as a cube's size; in other words, such a construct would violate the rule that the meaning of a member variable should not change from a parent class to any of its descendants), or you can decide not to use it at all and introduce three new member variables instead, ignoring the inherited "size" (but this is a bad thing too, because it leaves you with an unnecessary variable - this is both slightly wasteful and, much worse, potentionally misleading).

---
Me make music: Triofobie
---
"We need Tobias and his awesome trombone, too." - Johan Halmén

Go to: