underdoeg
underdoeg

Reputation: 1911

Building a c++ class from multiple components

I am working on a system where I have rectangles that can be stacked in a hierarchical order. So the base is something like this:

Rect parent;
Rect child;
parent.addChild(&child);
...
Rect* someChild = parent.getChildAt(1);

Easy to implement so far. But these rectangles should be able to implement different features. This could be a Serializer, Styler, Drawer, etc...

Now I know that multiple inheritance is a sort of "no go" but in this case I'd find a syntax like this desireable:

class StackableStyleableRect: publicRect, public Stackable, Styleable{}
class StackableStyleableDrawableRect: public Rect, public Stackable, Styleable, Drawable{}

I stumbled upon the curiously recurring template pattern (crtp) which would make the above possible if I understand it correctly. Something like this:

class Rect{
    public:
       float width;
       float height;
}

template <class RectType>
class Stackable{
    public:
        void addChild(RectType* c){
             children.push_back(c);
        }
        std::vector<RectType*> children;
}

template <class RectType>
class Drawable{
    public:
        virtual void draw(){
            RectType* r static_cast<RectType>(this);
            drawRect(r->width, r->height);
        }
}

template <class RectType>
class Styleable{
    public:
        int r, g, b;
}

class CustomRect: public Rect, public Stackable<CustomRect>, public Drawable<CustomRect>{
}

class CustomRectWithStyle: public Rect, public Stackable<CustomRect>, public Drawable<CustomRect>, public Styleable<CustomRect>{
    public:
}

The reason for this is that I would like to reuse the code with different kind of Rect types. In one project I don't have to style them while in another case I'd need all the functionality provided. With this way to select the features required, it would keep things clean and also separates funcitonality.

I did some basic tests with this and it works as expected but I feel like the syntax might grow overly complex over time.

Also at some point it would be useful to make components dependent from one another or make them behave differently if a component is around. (For example the draw function of Drawable could automatically use the colors from Styleable if present)

Now am I doomed to run into troubles sooner or later or might it work? Is there a different pattern that would fit better? Or is it simply not possible to do something like this in "proper" c++?

Upvotes: 3

Views: 963

Answers (3)

idoby
idoby

Reputation: 927

Let me point out some of the shortcomings of your approach to this problem before I suggest different solutions, each with its own advantages and disadvantages.

Shortcomings of the multiple inheritance approach

  • Inheriting from the Stackable class/interface builds knowledge of an aggregate data structure into a class that represents data (Rect). This might prove limiting in situations where Rects need to go into more than one data structure (A tree for lookups, for instance) or when a data structure needs to have Circles in it too. This solution also makes it difficult to swap out the data structure later.
  • Having lots of different class combinations can result in an unmanageable amount of classes that must be maintained over time.
  • Should someone else need to work on your code, but fail to notice that you've defined the type StackableStyleableDrawableRect. He or she then goes on to define their own DrawableStackableStyleableRect, that provides the same functionality but isn't the same as your class. In the best case scenario, you now have redundant code in your project. Worse off, you'll run into problems and confusion when you need to mix use of both classes because portions of the codebase already exist that use either.
  • Once another concern is introduced into the program, such as resizing Rects, do we change all of our existing classes or create further new ones? Do we change StackableStyleableDrawableRect into StackableStyleableDrawableResizeableRect, prompting changes to the existing codebase, or do we create it as an entirely new class?
  • Of course, with multiple inheritance, you run the risk of introducing the diamond problem if you're not careful, or if one day you decide that a Rect is both a GDIDrawable and a DirectXDrawable and needs to call something like Drawable::logDrawingOperation().

So while it may seem trivial that a Rect is-a Drawable, Stackable, etc, this approach is cumbersome and has many drawbacks. I believe that in this case, Rect has no business being anything other than a plain rectangle and shouldn't know about any other subsystem of the project.

Possible alternate solutions

Two alternate solutions exist that I can think of, but each of those makes the usual tradeoffs of readability vs. flexibility and compile-time complexity vs. run-time complexity.

Mix-ins

As illustrated here, using mix-ins through template cleverness can avoid some of the problems of the MI approach, though not all. On top of that, it creates a strange inheritance hierarchy and adds compile-time complexity. It also breaks down once we add more classes to the Shape hierarchy, since Drawable only knows how to draw a Rect.

Visitor pattern

The visitor pattern allows us to separate walking the object hierarchy from the algorithms that operate on it. Since each object knows its own type, it can dispatch the proper algorithm even without knowing what that algorithm is. To illustrate using Shape, Circle and Rect:

class Shape
{
    public:
        virtual void accept(class Visitor &v) = 0;
};

class Rect : public Shape
{
   public:
       float width;
       float height;

       void accept(class Visitor &v)
       {
           v.visit(this);
       }
};

class Circle : public Shape
{
    public:
        float radius;

       void accept(class Visitor &v)
       {
           v.visit(this);
       }
};

class Visitor
{
    public:
        virtual void visit(Rect *e) = 0;
        virtual void visit(Circle *e) = 0;
};

class ShapePainter : public Visitor
{
    // Provide graphics-related implementations for the two methods.
};

class ShapeSerializer : public Visitor
{
    // Provide methods to serialize each shape.
};

By doing this we have sacrificed some run-time complexity, but decoupled our various concerns from our data. Adding a new concern into the program is now easy. All we need to do is add another Visitor class that does what we want and use Shape::accept() in conjunction with an object of this new class, like so:

class ShapeResizer : public Visitor
{
    // Resize Rect.
    // Resize Circle.
};

Shape *shapey = new Circle();
ShapeResizer sr;
shapey->accept(sr);

This design pattern also has the advantage that if you forget to implement some data/algorithm combination but use it in the program, the compiler will complain. We may want to override Shape::accept() later to define, say, aggregate shape types like ShapeStack. This way we can traverse and draw the whole stack.

I think that if performance isn't critically important in your project, the Visitor solution is superior. It might also be worth considering if you need to meet real-time constraints but it doesn't slow down the program enough to jeopardize meeting the deadlines.

Upvotes: 3

voidpointercast
voidpointercast

Reputation: 185

Don't know if I understand your question correctly, but maybe policy based class design might be worth a look: http://en.wikipedia.org/wiki/Policy-based_design. It was first introduced in Alexandrescu's book Modern C++ Design. It allows to extend classes by deriving them from so called policies.

The mechanics looks like:

template<class DrawingPolicy> class Rectangle : public DrawingPolicy { ... };

where DrawingPolicy provides a draw(void) method for example, which is then available in class Rectangle.

Hope I could help

Upvotes: 2

Lol4t0
Lol4t0

Reputation: 12547

First of all,

I do not pretend to be extra C++ expert.

Multiple inheritance is not 'no go'

(otherwise it would be excluded from the language). Multiplle inheritance is the thing, that should be used carefully. And you should understand, what are you doing, when using it.

In your case it looks improbable that you meet diamond problem, that is evil of multiple inheritance.

Recursive template pattern allows you to check enabled features in compile time, like this

#define FEATURED(FEATURE, VALUE) \
template <template<class>class Feature = FEATURE> \
typename std::enable_if<std::is_base_of<Feature<RectType>, RectType>::value == VALUE>::type

template <class RectType>
class Styleable;

template <class RectType>
class Drawable{
    public:

        FEATURED(Styleable, true)
        drawImpl()
        {
            std::cout << "styleable impl\n";
        }

        FEATURED(Styleable, false)
        drawImpl()
        {
            std::cout << "not styleable impl\n";
        }


        virtual void draw(){
            drawImpl();
        }
};

You can implement something likewise in functionality with normal inheritance, but It seems impossible to make compile time feature checks.

On the other hand, you'll get your code more complicated with crtp, and you'll have to implement in all in header files.

Summing up,

I think it is complicated, and you should be sure you actually need it. It will work for some time, until it will be redesigned, as any other code. Its lifetime depends mostly on your task understanding.

Upvotes: 3

Related Questions