Sign in to follow this  

rtti question

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

Hi, I have read on these boards and elsewhere that using rtti and dynamic_cast can be a symptom of bad design. I'm using both in a few places and am wondering if mine is a case of 'ok usage' or 'bad design'. Here's an example. I have a typical console system with commands and variables. All the console commands and variables are handled by one list, so that tasks like registering and unregistering, autocompletion, and matching can be handled uniformly for both types. Classes Cmd and Var inherit from a common base class, and the ConList class maintains an array of pointers to this base class. Obviously there are some functions which apply to one subtype and not the other. For example, variables can be toggled, but not commands. So when the console receives a toggle command, it tries to find an entry with the given name, then uses rtti to determine if it is in fact of type Var, and then dynamic_casts the pointer to a Var* so it can call the Var function Toggle(). So anyway, that's what I'm doing - seems fairly harmless. But I'm not very experienced in software engineering, so, is this bad design? And if so, what would be a better solution?

Share this post


Link to post
Share on other sites
You could store them in two different lists. You'd still have a common base class though as things like the autocompletion is the same for both types. You can also do selective searching based on the current state, for example, you must have a command first, followed by a variable.

Thus, no RTTI is required, or dynamic_cast either.

Skizz

Share this post


Link to post
Share on other sites
Thanks for the reply. Even though this is knd of a trivial example, I'd like to understand the solution you're proposing, as similar problems may pop up elsewhere.

So are you recommending maintaining three lists - a base class list, a cmd list, and a var list - and using whichever is appropriate? Or, just using a cmd list and a var list and traversing them both when performing generic functions like autocompletion?

Any other thoughts will also be welcome.

Share this post


Link to post
Share on other sites
On the flip side of the coin, rtti is not always bad programming practice.
I agree that in most games, the speed hit involved in rtti is probably too large to be practical, but if speed is not a big issue, rtti does make some programming tasks considerably easier.
Quote:
Original post by jyk
So are you recommending maintaining three lists - a base class list, a cmd list, and a var list - and using whichever is appropriate? Or, just using a cmd list and a var list and traversing them both when performing generic functions like autocompletion?

Just remember that there is a third case, a class which is both command and variable (I am thinking stream re-direction in unix shells here, basically any command whose result could be passed as a variable to another command).

SwiftCoder

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
jyk,

Have you looked into the vistor design pattern? This is a good way to do what you want to do without using any rtti or any other messy stuff. The link I provided explains it pretty well, but you can find more by googling it. And feel free to ask if you find it confusing.

Share this post


Link to post
Share on other sites
Quote:
Original post by swiftcoder
Quote:
Original post by Anonymous Poster
The link I provided explains it pretty well, but you can find more by googling it.

The link, if you would be so kind?


The AP wasn't me, but here's the link to the google search:
Google search: "visitor design pattern"

Share this post


Link to post
Share on other sites
Quote:
Original post by jyk
Obviously there are some functions which apply to one subtype and not the other. For example, variables can be toggled, but not commands. So when the console receives a toggle command, it tries to find an entry with the given name, then uses rtti to determine if it is in fact of type Var, and then dynamic_casts the pointer to a Var* so it can call the Var function Toggle().


Here is a solution that might work for you: Add two functions, CanToggle and Toggle to the base class. It is kind of like RTTI but it distinguishes between functionality rather than type. That way the console code is no longer hard-wired to toggle only specific classes.

But I think the best solution is to maintain two lists as Skizz suggested.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
Quote:
Original post by JohnBolton
Here is a solution that might work for you: Add two functions, CanToggle and Toggle to the base class. It is kind of like RTTI but it distinguishes between functionality rather than type. That way the console code is no longer hard-wired to toggle only specific classes.


In a large and more complicated system this technique of throwing everything into one interface and having to make checks at runtime isn't a scalable solution. You're putting stuff in the interface which might do something but needs checking in case it doesn't.


#include <iostream>

class ConsoleEntity;
class Variable;
class Command;

class Visitor {
public:
virtual void Visit(ConsoleEntity* var) const {
//do nothing
//throw an exception
//whatever you want
}
//can't implement these here as don't have declarations of Variable and Command yet
//will implement it below
virtual void Visit(Variable* var) const;//calls Visit(ConsoleCommandBase* var) by default
virtual void Visit(Command* var) const;//calls Visit(ConsoleCommandBase* var) by default
};

class ConsoleEntity {
public:
void AutoComplete() {
std::cout << "Console Entity auto-completed" << std::endl;
}
virtual void Accept(const Visitor& visitor) {
visitor.Visit(this);
}
};

class Variable : public ConsoleEntity {
public:
void Toggle() {
std::cout << "Variable Toggled" << std::endl;
}
virtual void Accept(const Visitor& visitor) {
visitor.Visit(this);
}
};

class Command : public ConsoleEntity {
public:
void Execute() {
std::cout << "Command Executed" << std::endl;
}
virtual void Accept(const Visitor& visitor) {
visitor.Visit(this);
}
};

void Visitor::Visit(Variable* var) const {
ConsoleEntity* base = var;
Visit(base);
}
void Visitor::Visit(Command* cmd) const {
ConsoleEntity* base = cmd;
Visit(base);
}

class Toggle : public Visitor {
public:
virtual void Visit(Variable* var) const {
var->Toggle();
}
virtual void Visit(Command* cmd) const {
//call base Visitor::Visit(cmd)
//or do nothing, etc.
}
};

class Execute : public Visitor {
public:
virtual void Visit(Variable* var) const {
//do nothing, etc.
}
virtual void Visit(Command* cmd) const {
cmd->Execute();
}
};

class AutoComplete : public Visitor {
public:
virtual void Visit(ConsoleEntity* base) const {
base->AutoComplete();
}
};

int main() {
Variable v;
Command c;

Toggle t;
v.Accept(t);
c.Accept(t);

Execute e;
v.Accept(e);
c.Accept(e);

AutoComplete a;
v.Accept(a);
c.Accept(a);

//take pointers to base types
Visitor* visitor = &t;
ConsoleEntity* entity = &v;

entity->Accept(*visitor);//don't need to know types to call correct functions

return 0;
}


There are basically two calls to virtual functions to determine what function is finally called. E.g to get to call Toggle you call Visit on a base Console Entity class which determines whether it is a Command or Variable object. Then that object calls Accept on the Visitor using its own 'this' pointer. That determines which Accept function gets used.

Copy this into a new project and step through it to get a feel for what is happening.

There are probably different ways to use the Visitor Pattern in your case. I have made the Toggle, AutoComplete and Execute into Visitors but it may be that you can do something different.

Share this post


Link to post
Share on other sites
Quote:
Original post by petewood
*cough* that was me *cough*


A little nitpick of your code, you CAN implement Visitor::Visit(Variable *) and Visitor::Visit(Command *) at their definition since you're just dealing with the pointers at that point, not actually dereferencing them:

class ConsoleEntity;
class Variable;
class Command;

class Visitor {
public:
virtual void Visit(ConsoleEntity* var) const {
//do nothing
//throw an exception
//whatever you want
}
virtual void Visit(Variable* var) const { Visit((ConsoleEntity *) var); }
virtual void Visit(Command* var) const { Visit((ConsoleEntity *) var); }
};

Share this post


Link to post
Share on other sites
Quote:
Original post by MaulingMonkey
Quote:
Original post by petewood
*cough* that was me *cough*


A little nitpick of your code, you CAN implement Visitor::Visit(Variable *) and Visitor::Visit(Command *) at their definition since you're just dealing with the pointers at that point, not actually dereferencing them:


You're doing a C-style cast. static_cast would be more standard, although the whole point of this endevour is to avoid having to cast anything.

edit: You're making the assumption that Command and Variable are derived from ConsoleEntity. If that were to change then your code wouldn't complain. Mine would.

If the code was put into separate headers and cpp files it would be fine, but I wanted an example which could be easily copied and pasted into one file and compiled and run.


//Visitor.h
class ConsoleEntity;
class Variable;
class Command;
class Visitor {
public:
virtual void Visit(ConsoleEntity*) const;
virtual void Visit(Variable*) const;
virtual void Visit(Command*) const;
};






#include "Visitor.h"
#include "ConsoleEntity.h"
#include "Variable.h"
#include "Command.h"

void Visitor::Visit(ConsoleEntity*) const {
//throw
}
void Visitor::Visit(Variable* var) const {
ConsoleEntity* base = var;
Visit(base);
}
void Visitor::Visit(Command* cmd) const {
ConsoleEntity* base = cmd;
Visit(base);
}



Share this post


Link to post
Share on other sites
Quote:
Original post by petewood
You're doing a C-style cast. static_cast would be more standard, although the whole point of this endevour is to avoid having to cast anything.


Err, sorry to burst your bubble (if I am indeed doing that), but:

ConsoleEntity* base = var;


is just implicit casting instead of explicit casting. Going with static_cast would make my code complain as well (if Command and Variable didn't derive from ConsoleEntity).

It just threw me off when I saw Visit( ConsoleEntity * ) being implemented at that point, but not the other default instances of Visit... so I thought I'd comment. I like grouping code together.

Share this post


Link to post
Share on other sites
Quote:
Original post by MaulingMonkey
Quote:
Original post by petewood
You're doing a C-style cast. static_cast would be more standard, although the whole point of this endevour is to avoid having to cast anything.


Err, sorry to burst your bubble (if I am indeed doing that), but:

ConsoleEntity* base = var;


is just implicit casting instead of explicit casting.

Right, so if the truth of that relationship changed it would come up as an error. Yours wouldn't.

Quote:
Going with static_cast would make my code complain as well (if Command and Variable didn't derive from ConsoleEntity).
It wouldn't complain. And the cast is unnecessary as my code showed.

Quote:

It just threw me off when I saw Visit( ConsoleEntity * ) being implemented at that point, but not the other default instances of Visit... so I thought I'd comment. I like grouping code together.

I like grouping code together too. As I've explained this was a simple demonstration example so was all in one file. I wouldn't put it all in one file in 'real life'. I think that's pretty understandable and doesn't affect the soundness of the code. But I wouldn't use static_cast in real life and wouldn't want to give the impression that it was a reasonable thing to do.

Share this post


Link to post
Share on other sites
Quote:
Original post by petewood
Quote:
Original post by MaulingMonkey
Quote:
Original post by petewood
You're doing a C-style cast. static_cast would be more standard, although the whole point of this endevour is to avoid having to cast anything.


Err, sorry to burst your bubble (if I am indeed doing that), but:

ConsoleEntity* base = var;


is just implicit casting instead of explicit casting.

Right, so if the truth of that relationship changed it would come up as an error. Yours wouldn't.

Quote:
Going with static_cast would make my code complain as well (if Command and Variable didn't derive from ConsoleEntity).
It wouldn't complain. And the cast is unnecessary as my code showed.


I should have rephrased that as: it did complain (I tried it on GCC 3.3.1) as per standard-compliant behavior (the entire reason for having static_cast besides "consistancy" with the other *_cast operators). And your code is just a tad lengthier, and not quite as obvious at first glace as to it's function (IMHO).

edit: article about the new casts, and their functionality
edit: and my example testcase:
class BaseClass {};
class DerivedClass : public BaseClass {};
class UnrelatedClass {};

int main ( int argc , char ** argv )
{
DerivedClass * derivedclass;
UnrelatedClass * unrelatedclass;
//my version:
BaseClass * baseclass_1 = static_cast<BaseClass *>( derivedclass ); //compiles, no error
BaseClass * baseclass_2 = static_cast<BaseClass *>( unrelatedclass ); //main.cc:11: error: invalid static_cast from type `UnrelatedClass*' to type `BaseClass*'
//your version, without returns:
BaseClass * baseclass_3 = derivedclass; //compiles, no error
BaseClass * baseclass_4 = unrelatedclass; //main.cc:14: error: cannot convert `UnrelatedClass*' to `BaseClass*' in initialization
return 0;
}

Share this post


Link to post
Share on other sites
Hi MaulingMonkey,

Sorry, I was wrong about the compile time effect of static_cast.

Looking at my original code again, if you put the static_cast where you say it should go it won't compile because of the reasons you have said. There are forward declarations of Command, Variable and ConsoleEntity which don't express the relation between the types. The compiler does not have enough information to allow the static_cast to work. That was why the original c-style cast was so dangerous.

Using static_cast in place of my implicit conversion would possibly make it clearer that I am intending to make the compiler call the specific overload. I might use that in future.

This is getting away from the main thread, though. Thanks for the input.

Pete

Share this post


Link to post
Share on other sites

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