Jump to content
  • Advertisement
Sign in to follow this  
Numsgil

OO and hierarchies of virtual calls

This topic is 3775 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

This is half a question and half a free rambling essay, so bear with me. So consider this (contrived) C++ code:
struct Thing
{
    virtual void Work()
    {
        cout << "It's something ";
    }
};

struct Furniture : public Thing
{
   virtual void Work()
   {
        cout << "that is in your house ";
   }
}

struct Chair : public Furniture
{
    virtual void Work()
    {
        cout << "and you sit on it.  ";
    }
}

int main()
{
    Chair chair;
    chair.Work();
    return 0;
}







The output would be "and you sit on it. ". The target output I want is "It's something that is in your house and you sit on it. " That is, I want each work function to be chained together. At my company, this is the solution we're presently using:
struct Thing
{
    virtual void Hierarchy_Work()
    {
        cout << "It's something ";
    }
};

struct Furniture : public Thing
{
   virtual void Hierarchy_Work()
   {
        Thing::Hierarchy_Work();
        cout << "that is in your house ";
   }
}

struct Chair : public Furniture
{
    virtual void Hierarchy_Work()
    {
        Furniture::Hierarchy_Work();
        cout << "and you sit on it.  ";
    }
}

int main()
{
    Chair chair;
    chair.Work();
    return 0;
}







That is, it's a naming convention, where if you override a virtual function with the Hierarchy_ prefix, you have to call the base version. It seems to work fine in practice, but it's rubbing me the wrong way. It relies too much on the coder remembering to add calls to the base function when they override it, and generally feels a bit clumsy. Is there a cleaner way to arrive at this same end behavior? The ideal method would not rely on the end user needing to do anything more than override a virtual call and implement only the additional logic they want executed, and have all the overhead of calling base classes handled in some other way. (Also, it can't use any STL containers since my company is also STL averse, and likewise newing and deleting should be avoided). [Edited by - Numsgil on June 17, 2008 12:07:17 AM]

Share this post


Link to post
Share on other sites
Advertisement
My first thought would be some functor where you composite the methods and 'work' only exists in the base class (and is dealt with in the constructors of the derivations). Though it's still icky, and to do it right you'd probably have to use things a bit more hairy than the template library bits.

Share this post


Link to post
Share on other sites
First off, you'd never want to build a string like that since it does not internationalize nicely.

Secondly, there's a design problem if the base class assumes that whoever overrides the method must call the base implementation. I think the "Hierarchy_" tag is ridiculous. It is up to the child class to determine if they should propagate up to the parent.

If the base class wants to force this behavior what it should do is define a DoWork() that derived classes override and a Work() that calls DoWork() and does whatever else it needs to do.


Share this post


Link to post
Share on other sites
Quote:
Original post by AAA
First off, you'd never want to build a string like that since it does not internationalize nicely.


con·trived /kənˈtraɪvd/ –adjective
obviously planned or forced; artificial; strained: "a ~ story".

Quote:

Secondly, there's a design problem if the base class assumes that whoever overrides the method must call the base implementation.


It's not a problem if child classes actually do always call the base implementation. Hence why I said it works fine in practice. Obviously it breaks easily if a child class forgets, which is why I'm posting here to find a better way. We could ship our game just fine using the hierarchy_ system.

Quote:

I think the "Hierarchy_" tag is ridiculous. It is up to the child class to determine if they should propagate up to the parent.


In code it's up to the child class to decide, true, but logically that might not always be the case. As in my contrived (!) example, if I implement a Person class that inherits from Thing, I should logically expect my work function to do something like: "It's something that smells funny." That is, the work function on the base class should logically be called regardless of the derived class.

Quote:

If the base class wants to force this behavior what it should do is define a DoWork() that derived classes override and a Work() that calls DoWork() and does whatever else it needs to do.


Thought about this, but try working out what the code would look like in my Thing-Furniture-Chair contrived (!) example. Any inheritance chain longer than base class -> derived class requires some function somewhere in the nth derived class to still know about and call its base class counterpart. Meaning that the client is still writing (and potentially forgetting) BaseClass::Work().

Quote:

My first thought would be some functor where you composite the methods and 'work' only exists in the base class (and is dealt with in the constructors of the derivations). Though it's still icky, and to do it right you'd probably have to use things a bit more hairy than the template library bits.


Complicated template programming is something I can get away with if I can convince my co-programmers that it's better. Better in this case meaning less chance of breaking without significantly more typing. The anti-STL mood at the office has more to do with STL wrappers potentially new-ing resources during the middle of a level (personally I think there are easier ways to do this than dumping the standard C++ libraries, but I'm not the boss, so I don't get much say).

Share this post


Link to post
Share on other sites
What you might want is a Mix-In pattern, although it is one or two orders of magnitude more complex than your example.

You have a carrier (think ice-cream cone) which you put pieces into (think scoops of ice cream). First, you fetch a job type.


// JobRegistry.c
unsigned long JobRegistry::Job::next = 0;

// JobRegistry.h
static class JobRegistry
{
public:
class Job {
private:
static unsigned long next;
unsigned long id;
public:
bool operator == (const Job & other) const {
return id == id;
}

bool operator < (const Job & other) const {
return id < other.id;
}

Job() : id(next) { ++next; }
Job(const Job& other) : id(other.id) { }
};

private:
std::map<std::string, Job> map;

public:
const Job locateJob(const std::string name) {
std::map<std::string, Job>::iterator loc = map.find(name);

if ( loc != map.end() )
return *loc;

Job tmp();
return map[name] = tmp;
}

const Job locateJob(const char* name) {
std::string tmp(name);
return locateJob(tmp);
}

std::string getName(const Job& job) {
// Exersize for the reader.
}
} GJobRegistry;




Now you have a way of collecting simple, atomic, meaningful entities for fast comparison, which you can reverse-lookup for debug or serialization purposes later, and which you can create and store programmatically.

Next, you create your wrapper.


class Container
{
public:
void addWorker(Job& job, Worker& worker) {
// Store in an std::map<Job, std::list<Worker> >
}

void doWork(Job& job) {
// Locate the given job, and iterate over the workers,
// calling their work() methods as you go.
}

// Your 'container' holds more than workers; it also holds data for them
// to do their work with, and their results. A property bag is ideal.
};




Finally, you define your workers.

class Worker
{
public:
virtual void work(Container& where) = 0;
};




Then create workers that do useful tasks, initialize your containers through configuration, add workers appropriate for those tasks. Then, put a job into a container, and the job gets done.


class PrintWorkerFactory
{
class PrintWorker : public Worker
{
std::string output;
std::ostream* stream;
public:
PrintWorker(std::ostream& _stream, std::string& _output)
: stream(&_stream), output(_output) {}

virtual void work(Container& where) {
stream << output;
};
};

public:
Worker create(std::ostream& whom, std::string& what) {
PrintWorker tmp(whom, what);
return tmp;
}
} GPrintingFactory;

class PrintValueFactory
{
class PrintValue : public Worker
{
std::string name;
std::ostream* stream;
public:
PrintWorker(std::ostream& _name, std::string& _output)
: name(&_name), output(_output) {}

virtual void work(Container& where) {
// Assuming Container has a getValue(std::string) method.
stream << where.getValue(name);
};
};

public:
Worker create(std::ostream& whom, std::string& what) {
PrintWorker tmp(whom, what);
return tmp;
}
} GPrintValueFactory;



And finally, a usage of all that crap.

Container demo_container;
Job greet = GJobRegistry.locateJob("greet");
Job dismiss = GJobRegistry.locateJob("dismiss");

demo_container.addWorker( greet, GPrintingFactory(std::cout, std::string("Hello, ")) );

demo_container.addWorker( dismiss, GPrintingFactory(std::cout, std::string("See you later, ")) );
demo_container.addWorker( dismiss, GReadFactory(std::cout, "name") );
demo_container.addWorker( dismiss, GPrintingFactory(std::cout, std::string(".")) );

// Finish the greet job.
demo_container.addWorker( greet, GPrintingFactory(std::cout, std::string("what is your name?")) );

std::string input;
demo_container.doWork(greet);

std::cin >> input;
demo_container.setValue("name", input);

demo_container.doWork(dismiss);

Share this post


Link to post
Share on other sites
I can't think of anything off the top of my head that would simplify things for you. I can't help but wonder what the problem is you are trying to solve that requires this behavior. If you could give some context on the real problem you are trying to solve maybe we could come up with an alternative solution that avoids this problem in the first place.

Share this post


Link to post
Share on other sites
Quote:
Original post by bombinator-dev
I can't think of anything off the top of my head that would simplify things for you. I can't help but wonder what the problem is you are trying to solve that requires this behavior. If you could give some context on the real problem you are trying to solve maybe we could come up with an alternative solution that avoids this problem in the first place.


I don't think I can go too deep in to the specifics, because, like I say, it's for my company, and there's that whole trade secret clause in my contract. I doubt they consider the Hierarchy_ prefix a trade secret, but when you get too far in to the internals of the engine it's more of a gray area.

To be sufficiently vague, one level is for serialization. Another is for things that have a position in the world. Another is for things that can get drawn to the world. Another might be something like a particle emitter, on down like that. Each level may or may not respond to work events, draw events, development draw events, changes in data through serialization with the game editor, etc. etc. So the Hierarchy_ system is used all over the place. And I seriously doubt that I could convince anyone to change the inheritance hierarchy, even if I wanted to (which I most definitely do not. I'm not trying to rewrite the engine from scratch, I'm just trying to clean up my little section of insanity)

@Wyrframe: remember I said my company is STL averse. I could never get a std::map into the code. Even if I could, I doubt I could convince them it's better if it requires more effort on their part to interface with. Plus I can't help feel it's a jackhammer solution for a thumb tack problem (always at most one work function per inheritance level).

Share this post


Link to post
Share on other sites
In that case, your solution is approximately all there is. While LISP has the Flavours object system (an implementation of Mix-ins, like I gave above), C++ pretty much needs at least what I described above to implement mix-ins. It has no language-level support for that programming pattern.

If neither STL nor Boost are permitted, what is? Further, if neither of those are allowed, why are you working in C++ instead of C? No slight intended, it just seems completely backwards (which is far from atypical, given the nature of some workplace cultures).

Share this post


Link to post
Share on other sites
Quote:
Original post by Wyrframe
If neither STL nor Boost are permitted, what is? Further, if neither of those are allowed, why are you working in C++ instead of C? No slight intended, it just seems completely backwards (which is far from atypical, given the nature of some workplace cultures).


Well, let's say that my company uses C with classes, :) There's definitely a preference for things like qsort over std::sort. These are old game industry veterans who would very likely still be programming in assembly if they hadn't been drug kicking and screaming in to the world of compilers. They're not quite on board with the whole newfangled OO fad. :) From what I gather it's par for the course in the game industry.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!