0

I have a diamond inheritance scheme in C++ solved through virtual inheritance.

The base class is general has two attributes and some method using them.

class general {
    double attr1, attr2;
public:
    general(double attr1, double attr2) {
        this->attr1=attr1;
        this->attr2=attr2;
    }
    virtual double somemethod() {
        return attr1*attr2;
    }
};

A specialrestriction is a general with a special restriction that both attributes are equal.

class specialrestriction : public general {
public:
    specialrestriction(double attr) : general(attr, attr) {};
};

A specialrule slightly modifieds somemethod() with a special rule.

class specialrule : public general {
public:
    specialrule(double a, double b) : general(2*a, 2*b) {}
    double somemethod() override {
        return general::somemethod()*0.785398;
    }
};

Finally a restrictionandrule is a general with the same restriction as a specialrestriction (I would like to use its constructor), and the same special rule for computing the somemethod() as a specialrule (I would like to use its somemethod() method). The solution I have so far is:

class restrictionandrule : public virtual specialrule, public virtual specialrestriction {
public:
    restrictionandrule(double attr) : specialrestriction(attr), specialrule(attr,attr) {} //the call to the constructor of specialrule is (almost?) redundant
    double somemethod() override { return specialrule::somemethod(); } //only specialrule overrides somemethod(), is there another way to 
};

int main() {
    restrictionandrule c(5);
    std::cout<<"restrictionandrule::somemethod is " << c.somemethod()<<std::endl;
    return 0;
}

As put in the code's comments, I think the code of restrictionandrule is redundant. My intention when doing this was avoiding as much code redundancy as possible, but couldn't go any further.

(As a side comment, I'd like the language to solve the somemethod() ambiguity with less code (e.g., by calling the first parent's method by default), but to my knowledge that's just the standard, and this is not my question.)

My question is this: Since both specialrule and specialrestriction do little more (from the programmer's POV, I know I'm oversimplifying!) than calling the base constructor, I don't really need both of them, but C++ enforces calling both. Is there a better way to do this? Do I have this problem because I'm misunderstanding the `an A is a B' relationship? (perhaps because derived classes are not supposed to have less attributes than the base?)

JaMiT
  • 14,422
  • 4
  • 15
  • 31
Rafael
  • 129
  • 5
  • 2
    Personally I would make `circle` a specialisation of `ellipse` only. . Also I think `shape` should be abstract, and you should also have a `rectangle` class that `square` specialises. . Your hierarchy would then make more sense. – Den-Jason Jun 01 '22 at 22:36
  • 1
    Also don't forget to add virtual destructors to all of the classes. . And don't pull in any unnecessary includes in header files (e.g. `iostream`) – Den-Jason Jun 01 '22 at 22:38
  • Another aspect to consider is to use containment whenever possible, instead of inheritance – Den-Jason Jun 01 '22 at 22:51
  • 1
    Why is a circle a square??? Part of the definition of a square is having four straight sides; a circle does not. If you make your inheritance diagram match what you are representing, you might have fewer issues. – JaMiT Jun 01 '22 at 22:55
  • 2
    *"I have a diamond inheritance scheme"* -- no, you do not. Your `circle` class has two distinct `shape` base classes, one the base of `ellipse` and one the base of `square`. This is not a diamond but a 'V'. A diamond would make `shape` a virtual base class (not `ellipse` and `square`) so that there is only one `shape` per `circle`. See [How does virtual inheritance solve the "diamond" (multiple inheritance) ambiguity?](https://stackoverflow.com/questions/2659116/) – JaMiT Jun 01 '22 at 23:01
  • 2
    Maybe with `square` you are thinking of a mixin or concept. It's a shape where x and y dimensions are equal. As suggested you should have a rectangle and the `shape::area` method belongs in `rectangle`. Think about triangles, trapezes, 5 pointed stars, ... any other shape. None of them have an area of `base*height`. There is no good base formula for area so `shape::area` should be purely virtual. The square, circle, 5 pointed star would have the mixin/concept of equal width and height. – Goswin von Brederlow Jun 01 '22 at 23:09
  • 1
    Following the edit, I would contain `specialrule` and `specialrestriction` as members of `restrictionandrule`, given they are probably orthogonal. "Choose containment over inheritance" most likely prevents you having to deal with a diamond inheritance issue in 99% of cases. – Den-Jason Jun 01 '22 at 23:25
  • @Rafael I would recommend you read up on SOLID principles if you haven't done so already. e.g. https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design – Den-Jason Jun 01 '22 at 23:27
  • @Den-Jason That is a good (partial) answer I'm willing to accept. But I'm misunderstanding the 'an A is a B' relationship, or is it just too-specific a case that happens to be better to solve by containment rather than inheritance in C++ do to its syntax? – Rafael Jun 01 '22 at 23:30
  • @Den-Jason, thanks, I'll give it a look. I only had one formal OOP course in my undergraduate studies (quite a few years ago), and I certainly may be missing some concepts. – Rafael Jun 01 '22 at 23:31
  • 1
    As a rule of thumb I find it useful to think of "use inheritance ONLY when I need to specialise behaviour" and use containment for all other cases. The SOLID principles will reinforce this in your mind. – Den-Jason Jun 01 '22 at 23:36
  • 1
    @Rafael I also recommend the book "Design Patterns Explained" by Shalloway and Trott, which uses the concept of "Commonality and Variability analysis" throughout. That provided me a big improvement in how I architected systems - https://www.amazon.co.uk/Design-Patterns-Explained-Perspective-Object-Oriented/dp/0321247140 – Den-Jason Jun 01 '22 at 23:44

2 Answers2

1

This is how I would implement the hierarchy

#define _USE_MATH_DEFINES         //for older compilers
#include <cmath>

class shape {
public:
    virtual ~shape() {}
    virtual double area() = 0;
};

class rectangle : public shape {
protected:
    double mBase;
    double mHeight;

public:
    rectangle(double base, double height) : mBase(base), mHeight(height) {}
    virtual ~rectangle() {}
    virtual double area() override { return mBase*mHeight; }
};

class square : public rectangle {
public:
    square(double side) : rectangle(side, side) {};
    virtual ~square() {}
};


class ellipse : public shape {
protected:
    double mMajor;
    double mMinor;

public:
    ellipse(double major, double minor) : mMajor(major), mMinor(minor) {}
    ~ellipse() {}
    virtual double area() override { return mMajor * mMinor * M_PI; }
};

class circle : public ellipse {
public:
    circle(double radius) : ellipse(radius,radius) {}
    virtual ~circle() {}
};

// --------------------------------------------------------------
#include <iostream>
#include <memory>

int main() {            
    std::unique_ptr<shape> widget = std::make_unique<circle>(2.0);
    std::cout << "Area is: " << widget->area() << "\n";
}

Demo: https://godbolt.org/z/4bTscEEvT

Next step would be to template the types.

You could use a std::pair instead of having mBase, mHeight etc, but then you would be sacrificing readability for a dubious level of avoiding duplication.

Den-Jason
  • 2,395
  • 1
  • 22
  • 17
  • Uh, sorry for the edits, I appreciate your answer. I was just a bit bugged that the problem was not in the underlying ontology, but in the application of multiple inheritance. – Rafael Jun 01 '22 at 23:19
1

Just because you aren't actually using specialrestriction doesn't mean you can just leave it uninitialized. For every object of a class type, its constructor will be called upon construction (and its destructor upon destruction). In general the compiler can't know that the body of specialrestriction's constructor is empty (for instance, if it was defined in a different compilation unit), so failing to call it could easily violate specialrestriction's assumptions about its internal state.


Note that you haven't correctly implemented virtual inheritance. Your hierarchy still has two instances of general. An object of type restrictionandrule looks like this:

┌─────────────────────────┐
│restrictionandrule       │
│ ┌─────────────────────┐ │
│ │specialrule          │ │
│ │ ┌─────────────────┐ │ │
│ │ │general          │ │ │
│ │ │                 │ │ │
│ │ │                 │ │ │
│ │ │                 │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
│                         │
│ ┌─────────────────────┐ │
│ │specialrestriction   │ │
│ │ ┌─────────────────┐ │ │
│ │ │general          │ │ │
│ │ │                 │ │ │
│ │ │                 │ │ │
│ │ │                 │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
└─────────────────────────┘

Each of the specialrule and specialrestriction syb-objects has its own general sub-object, and so you need a separate constructor call to initialize each of them separately. In fact, in your specific example your two general sub-objects have different values for attr1 and attr2. This is also why you need to explicitly override somemethod in restrictionandrule. Without your help the compiler doesn't know which direct parent you want to call it on.

To eliminate the extra general sub-object, specialrule and specialrestriction need to inherit virtually from general (restrictionandrule does not need to inherit virtually from specialrule and specialrestriction though).

Miles Budnek
  • 28,216
  • 2
  • 35
  • 52