Sign in to follow this  

RTTI system in C++

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

I'm currently in the need of a (simple) RTTI system for my current project. At this point I use #defines and a DWORD value(Which is returned by TypeOf()). However, using this method is pretty much error prone(By small mistake one can define the same number twice), so I was looking for something better. Any ideas, that don't require STRANGE magic? Toolmaker

Share this post


Link to post
Share on other sites
The Loki library has a full TypeInfo class which makes it possible to compare type_info objects.

My usual question is, what are you using it for? It is quite possible (even likely) that using RTTI is the wrong solution to the problem.

Share this post


Link to post
Share on other sites
Fruny, what do you mean by that? Got any examples? Is that boost or something?

Petewood: I'm building a server. When storing all the clients, I make no difference between a client that's logging in, a client that's connected and NPCs. It's all just 1 big list.

However, when a user that's logging in is done, it returns a new object(A CUser object) which I push back into the list, and then delete the login session from the list. At this point, my update loop looks like this(Psuedo code)


for (iterate client list)
{
ret = client->Update();
if (ret < 0)
{
OnClientDisconnect(client);
}
else if (ret > 0)
{
if (client->TypeOf() == LoginSession)
OnClientLogin(client);
else
OnChatMessage(client);
}
}


The loop is going to be a bit bigger to support commands aswell, but basicly I need to be able to determine the type in some cases.

Toolmaker

Share this post


Link to post
Share on other sites
Quote:
Original post by Toolmaker

for (iterate client list)
{
ret = client->Update();
if (ret < 0)
{
OnClientDisconnect(client);
}
else if (ret > 0)
{
if (client->TypeOf() == LoginSession)
OnClientLogin(client);
else
OnChatMessage(client);
}
}


The loop is going to be a bit bigger to support commands aswell, but basicly I need to be able to determine the type in some cases.

Toolmaker

Why isn't all that logic inside the client iself?

class Client {
void update () {
// OnChatMessage stuff
}
};

class LoginSession: public Client {
void update () {
// OnClientLogin stuff
}
};

for (client in client_list) {
client.update();
}

Either the loop you posted is missing some important feature that's present in the real one, or you're doing things the wrong way.

That said:

if (dynamic_cast<LoginSession*>(client))
OnClientLogin(client);
else
OnChatMessage(client);


However, it looks like you need to destroy the client object once it logs in. That seems a bit silly, to me.

Rule of thumb: When you're switching on types, it will usually be better to dispatch to virtual functions instead.

Share this post


Link to post
Share on other sites
Quote:
Original post by Toolmaker
Fruny, what do you mean by that? Got any examples? Is that boost or something?


No, it's pure C++ RTTI. The C++ typeid operator returns a std::type_info object which contains, well, type information. You can compare two such objects to detect whether two objects are of the same (dynamic) type.

For your current problem, there are much better solutions: virtual functions


class AbstractClientSession
{
public:
virtual ~AbstractClientSession() {};
virtual int Update() = 0;
virtual void Disconnect() = 0;
virtual void Handle() = 0;
};

class LoginSession : public AbstractClientSession
{
int Update() {}
void Disconnect() { /* OnClientDisconnect(*this); */ }
void Handle() { /* OnClientLogin(*this); */ }
}
};

class ChatSession : public AbstractClientSession
{
int Update() {}
void Disconnect() { /* OnClientDisconnect(*this); */ }
void Handle() { /* OnChatMessage(*this); */ }
};

...
// client is of type AbstractClientSession*

for (iterate client list)
{
ret = client->Update();
if (ret < 0)
{
client->Disconnect()
}
else if (ret > 0)
{
client->Handle()
}
}


Quote:
The loop is going to be a bit bigger to support commands aswell, but basicly I need to be able to determine the type in some cases.


If you're just going to do the equivalent of a switch on the type, you don't need explicit RTTI. You use virtual functions through a common interface. Worst case, if you need to call member functions which are not part of the base class, you use a dynamic_cast, but that should never be your first choice.

Share this post


Link to post
Share on other sites
I second (or is it third? (c:) what Mayrel and Fruny said.

If you are switching on the type of an object it is probably better to use virtual functions instead.

Get a copy of Refactoring: Improving the Design of Existing Code.

One of the refactorings is Replace Conditional With Polymorphism.
Also see the Wiki entry ReplaceConditionalWithPolymorphism

Share this post


Link to post
Share on other sites
Well, I DO have a CClientBase class, from which I derive the LoginSession and User class. However, I implemented some events in both the server controller and the client class. The reason is simple:

OnClientLogin iterates the entire client list and calls the event for each client. OnClientDisconnect does the same and also cleans up the client that disconnected. This makes the main loop a little cleaner by moving all the work to functions.

The reason I have the server controller do this kind of work is because the controller is actually storing the clients, the listen socket, etc. But basicly, the events just reroute messages from the triggering client to the other clients.

Ofcourse, this brings in 1 other question: Should I move the loginsession handling code into the class itself AND give that class access to the user list, OR just keep it inside the server as it is now?

Toolmaker

Share this post


Link to post
Share on other sites
Quote:
Original post by darookie
How about function overloading then?

void Server::Handle(Client *client);
void Server::Handle(LoginSession *login);



...
else if (ret > 0)
{
Handle(client);
}


That might actually be the solution! Lemme implement that.

Toolmaker

Share this post


Link to post
Share on other sites
Quote:
Original post by Toolmaker
Well, I DO have a CClientBase class, from which I derive the LoginSession and User class. However, I implemented some events in both the server controller and the client class. The reason is simple:

OnClientLogin iterates the entire client list and calls the event for each client. OnClientDisconnect does the same and also cleans up the client that disconnected. This makes the main loop a little cleaner by moving all the work to functions.

I'm confused... You have an outer loop that iterates through the entire client list and each iteration calls a function that itself iterates through the entire client list to find the client that was passed to it? Eh?
Quote:

The reason I have the server controller do this kind of work is because the controller is actually storing the clients, the listen socket, etc. But basicly, the events just reroute messages from the triggering client to the other clients.

Ofcourse, this brings in 1 other question: Should I move the loginsession handling code into the class itself AND give that class access to the user list, OR just keep it inside the server as it is now?

Toolmaker

Well, what's unclear to me is how you login at all.

CClientBase::Update appears to return an integer which is magically interpreted as a request to login when it is positive.

I'd suggest something vaguely like:

class Command
{
bool checkUser (Client* c)
{
if (!c->user) {
c->sock->send("Login first.");
return false;
}
return true;
}
};

class Speak: Command
{
void doIt (Client* c)
{
if (!checkUser(c))
return;
...
}
}

class Login: Command
{
string user;
string password;
void doIt (Client* c)
{
User* u = User::checkUser(user, password);
if (u) {
c->user = u;
c->sock->send("Welcome to the thing, %s.", user->name);
}
}
};

class Client
{
User* user;
Socket* sock;
void process ()
{
Command cmd = Command::parseCommand(sock->getLine());
cmd.doIt(this);
}
};

Share this post


Link to post
Share on other sites
Lemme show some code to clear things up:

Server Controller:

list<CClientBase *>::iterator it;
CClientBase *pClient = NULL;

for (it = m_ClientList.begin(); it != m_ClientList.end(); ++it)
{
int nRet = 0;
pClient = *it;
nRet = pClient->Update();

if (nRet < 0)
{

}
else if (nRet > 0)
{
if (pClient->TypeOf() == LoginSession)
{
// Get the user from the login session
CLoginSession *pSession = (CLoginSession *)pClient;
CUser *pUser = pSession->GetUser();
m_ClientList.push_back(pUser);

// Delete the login session
it = m_ClientList.erase(it);
delete pSession;

// And fire the user login event!
this->OnClientConnect(pUser);
}
else
{
this->OnChatMessage(pClient);
}
}
}



And here are the events, off the server controller:

// OnClientConnect
// A user has successfully logged in on the server
void CServerController::OnClientConnect(CClientBase *pClient)
{
list<CClientBase *>::iterator it;

for (it = m_ClientList.begin(); it != m_ClientList.end(); ++it)
(*it)->OnClientConnect(pClient);
}

// OnChatMessage
// Either a user or NPC said something
void CServerController::OnChatMessage(CClientBase *pClient)
{
list<CClientBase *>::iterator it;
string str;

// Get the message from the client
pClient->GetBuffer(str);

// Iterate all clients and send the message
for (it = m_ClientList.begin(); it != m_ClientList.end(); ++it)
(*it)->OnChatMessage(str, pClient);
}



Does that explain things better?

Toolmaker

Share this post


Link to post
Share on other sites
Ah right, I see.


for (list<Client *>::iterator it = clients().begin();
it != clients().end();
++it)
{
Client *client = *it;
int ret = client->Update();
// It's not clear to me that Update returns <0 for logout and >0 for
// something else.
// Perhaps use an enum...
switch (ret)
{
case LOGOUT:
// Do whatever
break;
case SPEAK:
// It isn't clear to me why the user hasn't already been logged in
// if we already know his user name...
if (LoginSession* session = dynamic_cast<LoginSession *>(client))
{
User* user = session->getUser();
// Just replace the old value. Has two bonuses: firstly, only one
// iteration through the sequence (which is costly on lists);
// secondly, the user doesn't get a free iteration at the end of
// this command loop.
*it = user;
delete session;
call_every(clients, &Client::onOtherConnect, user);
} else {
call_every(clients, &Client::onMessageRcvd, client,
client->getBuffer());
}
}
}

// A couple of utility functions for neatly calling a member function on each
// element of a list.

// With two arguments.
template<typename Tret, typename Tseq, typename Tfn,
typename Targ1, typename Targ2>
Tret call_every (Tseq seq, Tfn fn, Targ1 arg1, Targ2 arg2)
{
for_each(seq.begin(), seq.end(), bind<void>(fn, _1, arg1, arg2));
}

// With one argument.
template<typename Tret, typename Tseq, typename Tfn,
typename Targ1>
Tret call_every (Tseq seq, Tfn fn, Targ1 arg1)
{
for_each(seq.begin(), seq.end(), bind<void>(fn, _1, arg1));
}

Share this post


Link to post
Share on other sites
*Swoops in to post the obligatory link to article*
Hungarian wartHogs.

Actually, there's hardly any HN there. p is the only one I can see (maybe n in nRet but if so I don't know what it's supposed to mean. Number?). I'd be more inclined to comment on the non-communicative variable names. What exactly is nRet?

Enigma

Share this post


Link to post
Share on other sites
Well, the < 0 and > 0 are "standard" winsock return types at this point. -1 is for socket error or disconnect, and 1 for data pending on the socket. I am going to change it later on, since the user can specify how commands are interpretted(Either by using a . or / or to interpret everything standard as command).

And what's up with hungarian notation? It's just how I code... In fact, at my student job(I usually work 1 day in the week as R&D programmer) they use it aswell. If I write code in C#, I don't use hungarian notation but use the C# "standard", and the same accounts for Java(College assignment, etc.)

Toolmaker

Share this post


Link to post
Share on other sites
1 more question, I replaced some code to this:

else if (nRet > 0)
{
if (CLoginSession *pSession = dynamic_cast<CLoginSession *>(pClient))
{
(*it) = pSession->GetUser();
delete pSession;
this->OnClientConnect((*it));
}
else
{
this->OnChatMessage(pClient);
}
}


And now it crashes horribly with some _non_rtti_object exception. Perhaps I should dump it all and start using a proper switch a la:

switch (nRet)
{
case DISCONNECT: break;
case LOGIN: break;
case CHAT: break;
case COMMAND: break;
case IMARETARD: pWorld->Explode(); break;
}

Share this post


Link to post
Share on other sites
I'm guessing you either forgot to turn on RTTI when compiling your project or you forgot to give your class virtual functions in order to enable RTTI for the individual types. More likely the former than the latter, if your compiler didn't give you a warning about using dynamic_cast.

Share this post


Link to post
Share on other sites
In my experience, probably the latter. My compiler always turns on RTTI when it might be needed (why wouldn't it?).

Rule of thumb: If there could ever be a situation when you delete an object via a pointer to an object of a supertype, give it a virtual destructor.


class Client
{
public:
virtual ~Client () { ... }
};

class LoginSession: public Client
{
public:
virtual ~LoginSession () { ... }
};

Share this post


Link to post
Share on other sites
Quote:
Original post by Enigma
*Swoops in to post the obligatory link to article*
Hungarian wartHogs.

Actually, there's hardly any HN there. p is the only one I can see (maybe n in nRet but if so I don't know what it's supposed to mean. Number?).

Well, there aren't that many names used. Five of the seven variable names use HN, the exceptions being 'it' and 'm_ClientList'. And CClientBase has C prefixed, which is HN.

The thing with HN is that it adds no value to the name. You can tell that 'pClient' is a pointer by looking at its declaration. If you're confused, a competent IDE will show you the type of 'pClient' without you needing to hunt for it.
Quote:

I'd be more inclined to comment on the non-communicative variable names. What exactly is nRet?

The problem there is not the variable name. Consider a descriptive version of the name, dataPendingIfOneDisconnectedIfNegative. That accurately describes how the variable behaves. But it hardly makes it much clearer.

enum CommandType
{
DISCONNECT,
LOGIN,
SPEAK,
...
};
switch (client->Update())
{
case NONE:
...

It's not particularly obvious that Client::Update returns a command type. Although it's sometimes okay for a function's return value to be surprising, it's never okay for it to be surprising after you know what it is. This implies that Client::Update is misnamed.

enum CommandType
{
NONE, // There might not be a command waiting, and you don't want to block.
DISCONNECT,
LOGIN,
SPEAK,
...
};
switch (client->GetNextCommand())
{
case NONE:
...

Another rule of thumb: If a variable can only take on one of a limited set of values and isn't intended to be used for arithmetic, it's usually clearer to use an enumeration.

Share this post


Link to post
Share on other sites
I've actually got a lot more time for the idea of Hungarian Notation than I used to. You have to see it in context.

I'm not advocating its use now, by the way, I'm just going to point out a few things.

The Hungarian in question is Charles Simonyi who worked for Microsoft in the early days and parted company with them in 2001. People say now 'you don't need HN because the IDE can tell you stuff', etc. But actually back then there weren't browsers which could aid you. C functions were often long as opposed to C++ member functions which are usually shorter. It was easy to lose sight of what variables were 3 screens earlier, etc.

The intention behind Hungarian Notation was to make the code communicate as much as possible to the (human) reader.

Simonyi went on to develop the Intentional Programming environment, which is probably not going to see the light of day, but is very cool non-the-less. The principles of IP look as though they will be incorporated in InteliJ IDEA, which is already a brilliant tool.

See this article on Language Oriented Programming: The Next Programming Paradigm by the InteliJ developer.

Share this post


Link to post
Share on other sites
Quote:
Original post by petewood
I've actually got a lot more time for the idea of Hungarian Notation than I used to. You have to see it in context.

I'm not advocating its use now, by the way, I'm just going to point out a few things.

The Hungarian in question is Charles Simonyi who worked for Microsoft in the early days and parted company with them in 2001. People say now 'you don't need HN because the IDE can tell you stuff', etc. But actually back then there weren't browsers which could aid you. C functions were often long as opposed to C++ member functions which are usually shorter. It was easy to lose sight of what variables were 3 screens earlier, etc.


Also, without std::string around, you had to be able to distinguish between a char * intended for use as "cp" (pointer to a single char, or to some number of chars known by external means) from one intended for use as "sz" (keep reading it until you find a \0). So it did actually add some type information. [smile] Just another amusing relic now, though.

(Ironically enough, I've been tempted on more than one occasion to introduce HN into my Python code, since the type names wouldn't otherwise be present. So far I've resisted, though. Explicit type checks and asserts seem to do the trick.)

Share this post


Link to post
Share on other sites

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