Sign in to follow this  

[C++] class hierarchy design

This topic is 2587 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello all,

I've been doing some code redesign lately, and have come across a design issue I can't seem to answer easily myself. Suppose I have a simple OpenGL wrapper class, for example:
class OpenGL : public GraphicsDriver
{
public:
...
virtual void depthMask(GLboolean flag) { glDepthMask(flag); }
virtual void begin(GLenum mode) { glBegin(mode); }
virtual void end() { glEnd(); }
...
};


Then I have a derived class supporting logging:
class OpenGLLogger : public OpenGL
{
public:
...
virtual void depthMask(GLboolean flag) { printf("glDepthMask(%d)\n", flag); OpenGL::depthMask(flag); }
virtual void begin(GLenum mode) { printf("glBegin(%d)\n", mode); OpenGL::begin(mode); }
virtual void end() { printf("glEnd()\n"); OpenGL::end(); }
...
};


And I also have another derived class supporting state caching (this is obviously a very simple example):
class OpenGLCaching : public OpenGL
{
public:
...
virtual void depthMask(GLboolean flag) { mDepthMask = flag; }
virtual void begin(GLenum mode) { flushState(); OpenGL::begin(mode); }
...
private:
void flushState() { OpenGL::depthMask(mDepthMask); ... }
};


And so, my two questions:

1) As an end-user, if I was given this code (which is already built in a library or DLL that I cannot modify), is there any easy way for me to utilize the caching ability of the OpenGLCaching class, yet also allow logging through the provided OpenGLLogger class? My guess is "no", since I can't just derive from both classes -- the compiler won't know what virtual function to call in what order.

2) (Assuming #1 is no) As the developer, is there any design pattern I can use that would allow an end-user to be able to perform something like the above example?

Perhaps some of the C++ gurus out there would have a better suggestion. Thanks!

Share this post


Link to post
Share on other sites
Maybe policy-based composition (I don't recall the 'official' pattern name)? Something like
template< typename LogPolicy = NoLogging, typename StatePolicy = NoState >
class OpenGL : public GraphicsDriver, private LogPolicy, private StatePolicy {
public:
void begin(...) {
LogPolicy::begin(...);
StatePolicy::begin(...);
glBegin(...);
}
};

Then you create a policy for each type of functionality and combine them in the final class. This has the added advantage that you can easily create and interchange multiple behaviors, for example logging to command line, logging to a file, logging to a remote network machine would be three different policy implementations and the user can select to most appropriate for his usage scenario.

Share this post


Link to post
Share on other sites
Optionally support both in the base class? This seems like a bad use for inheritance, precisely because of the problem you're having.

Maybe make OpenGLCaching derive from OpenGL, but make both of them accept an optional logger object. If you want to be all patterny, you could make the logger default to a NullObject. That would at least look nicer than saying if(logger) logger->log(blah) all over the place.

Share this post


Link to post
Share on other sites
It's a fairly common issue of cross-cutting orthogonal functionality. There is no accepted solution for C++ although attempts were made. Most are associated with Aspect Oriented Programming (AOP).

Main reason why it doesn't fit well with C++ is the static compilation model. This type of design almost requires introspective and run-time code modification capabilities. In C++ these are typically done at compile-time using special pre-processing and code generation.

The reason it's so complicated is because each orthogonal functionality multiplies complexity. So m by n for just two aspects.

And since inheritance allows merely all-or-nothing, one cannot limit extensions to arbitrary subsets, such as just functions with certain signature or behavior.



tl;dr; Cure is worse than disease. Either messy code generation or tons of proxy classes gluing together different functionality.

Share this post


Link to post
Share on other sites
First of all, thanks everyone for your comments and suggestions.

Quote:
Original post by Antheus
Main reason why it doesn't fit well with C++ is the static compilation model. This type of design almost requires introspective and run-time code modification capabilities. In C++ these are typically done at compile-time using special pre-processing and code generation.

This is the real answer I was looking for, actually -- sadly, it's just not something easily done with C++.

I did some reasearch on Aspect Oriented Programming, and found FeatureC++, which looks like it would do what I'm trying to accomplish. I'll have to play with it. An implementation of the Decorator pattern seems like it would be possible too, but object creation would certainly look ugly.

Quote:
Original post by theOcelot
Optionally support both in the base class? This seems like a bad use for inheritance, precisely because of the problem you're having.

My intent is to design an interface that allows the user to think out of the box. The user might want to do something completely different -- like say, pass the OpenGL commands through a network socket. Ideally, the user should still be able to use my provided cached implementation, and still be able to extend the functionality as he/she desires. I can't just limit them to a template with or without logging, for example.

Share this post


Link to post
Share on other sites
I'm not a fan of that solution (due to runtime performance), but ...
Quote:
Original post by bpoint
...An implementation of the Decorator pattern seems like it would be possible too, but object creation would certainly look ugly.
What do you mean? Its the typical pattern used by streams. In the given use case for example:
// the graphics engine
GraphicsDriver *graphics = new OpenGL();
// if decided to use a logger
graphics = new GraphicsDriverLogger( graphics );
// if decided to use caching
graphics = new GraphicsDriverCaching( graphics );



You just need a couple of classes that ensure correct assembling:
class GraphicsDriver {
...
virtual void depthMask(bool flag) =0;
...
};

class GraphicsDriverDecorator
: public GraphicsDriver {

GraphicsDriverDecorator( GraphicsDriver *sink )
: _sink( sink ) { ... }

GraphicsDriver *sink() const {
return this->_sink;
}

...
virtual void depthMask(bool flag) {
this->_sink->depthMask( flag );
}
...
};

class GraphicsDriverLogging
: public GraphicsDriverDecorator {

GraphicsDriverLogging( GraphicsDriver *sink )
: GraphicsDriverDecorator( sink ) { ... }

...
virtual void depthMask(bool flag) {
this->_logging->logDebug( ... );
GraphicsDriverDecorator::depthMask( flag );
}
...
};

class OpenGL : public GraphicsDriver {
...
virtual void depthMask(bool flag) {
::glDepthMask( flag );
}
...
};

Share this post


Link to post
Share on other sites

This topic is 2587 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this