Sign in to follow this  
jmp97

Smart pointer design issues, ownership, and efficiency

Recommended Posts

Hello, My question concerns shared_ptr, intrusive_ptr and the design issues involved with them in the given scenario. I have searched the forums but didn't find all the answers to my questions yet. I would like to illustrate that better by providing the specific scenario first. The Scenario I am writing a game server, the system is single-threaded. The game server should manage the following game entities: - users - characters - world A user "has" one selected character which represents the player character object in the game (may have no selected character also). A character can also be owned by a bot (NPC) but not by a user and a bot at the same time. A character should know its owning user to avoid frequent lookups (which inroduces a cyclic reference). A world object holds maps of all logged-in users and of all active (currently playing) characters for easy id-based lookup and access to all live entities. In this design, in order to avoid memory leaks, I was simply replacing raw pointers with shared_ptr objects to the newed pointers. Initial simplified approach
class User {
	shared_ptr<Character> selectedCharacter;
}

class Character {
	shared_ptr<Character> selectedCharacter;
	shared_ptr<User> owningUser;
}

class World {
	map< unisgned int, shared_ptr<User> > liveUsers;	// containing logged-in users
	map< unisgned int, shared_ptr<Characer> > liveCharacters;	// containing selectedCharacters of users (or bots)
}


Cyclic reference The first thing would be to resolve the cyclic reference and replace
shared_ptr<User> owningUser;


with
weak_ptr<User> owningUser


in the Character class to avoid that the objects are never automatically freed. Ownership It seems that a character is always owned by a user (or a bot) but not by the world. This is because when a user logs out, the selectedCharacter tied to this user will be remove from the world's liveCharacters map. The world lasts for as long as the server is running so it does not participate in the lifetime of the liveCharacters it holds even if it should. Now the question is: Should the world be storing smart pointers in the liveCharacters map, or rather references to the character objects (or raw pointers)? If so, maybe there is no reason to have a shared_ptr to the character in the user at all because ownership is not shared. Is that a valid conclusion? auto_ptr and reference to heap object I could then store the selectedCharacter in the user as an auto_ptr to just make sure that I don't need to call delete when it goes out of scope. Concerning the world I could store a reference to the character heap object in the map (careful not to mix auto_ptr with stl containers).
class User {
	auto_ptr<Character> selectedCharacter;	// now auto_ptr!
}

class World {	
	map< unisgned int, shared_ptr<User> > liveUsers;
	map< unisgned int, Character& > liveCharacters;	// now map to heap object reference!
}


The liveUsers would still be contained in shared_ptr objects because the world can be considered to own them and I want to store it into a std::map (which auto_ptr shoul not be used for). Am I still on track with such a setup? Returning shared_ptr vs. returning raw pointers / references I still store shared_ptr objects in the liveUsers map of the world. The world offers a function to get a user for a given id:
typedef boost::shared_ptr<User> SPUser;
...

SPUser World::getLiveUser(unsigned int userId) {
	SPUser u;
	std::map<unsigned int, SPUser>::iterator it = liveUsers.find(userId);
	if (it != liveUsers.end()) {
		u = (*it).second;	// user is logged in
	}
	return u;
}


I return an empty shared_ptr when the user is not found and a valid shared_ptr otherwise. Should I be returning a shared_ptr at all? Wouldn't it be more generic if other code would deal with a raw pointer, this would also save reference counting locking operations and the like? On the downside, other code *could* call delete on the pointer. Also, it may be possible that the shared_ptr reference count reaches zero while the raw pointer is still in use elsewhere - but as long as I do not store the raw pointer and everything is single-threaded this should not be an issue? On the other hand coming up with such restrictions may hint at a design problem? Passing a shared_ptr vs. shared_ptr reference vs. heap object reference
typedef boost::shared_ptr<User> SPUser;
typedef boost::shared_ptr<const User> SPConstUser;
...

void readUser(SPConstUser u) {
	unsigned int id = u->getId();
	// ...
}

void modifyUser(SPUser u) {
	u->modify();
}


Here, passing a shared_ptr reference would circumvent the reference counting. But then again since this is single-threaded I can be sure that the shared_ptr reference count will not be decreased as long as I am not doing anything inside the function which would give rise to it (e.g. by calling another function which removes the share_ptr user object from the liveUsers map). Then, I could also just pass a reference to the user heap object itself? This way the function will not worry about smart pointer or not, reference count, etc. and it seems more generic. Efficiency and intrusive_ptr The reason why I am asking all this is also partly because in my current design I am passing and returing shared_ptr objects by value everywhere which could amount to some overhead with many objects live on the server. If worrying about efficiency here I could also use intrusive_ptr but then again I have these doubts if I really need to have shared_ptr objects everywhere or could restrict it to where I really need it. What I don't like also is that I have to adjust the interfaces in the rest of the code to deal with shared_ptr objects instead of with just the generic object type references. I hope I didn't ask too many questions at once ;) Any suggestions are highly welcome! [Edited by - jmp97 on June 5, 2009 4:22:37 AM]

Share this post


Link to post
Share on other sites
Quote:
I am writing a game server, the system is single-threaded.
[...]
The reason why I am asking all this is also partly because in my current design I am passing and returing shared_ptr objects by value everywhere which could amount to some overhead with many objects live on the server.
[disclaimer]I didn't read the middle part of your post, as it is rather looooong[/disclaimer]

Generally, shared_ptr is something that works fine for 99% of the people 99% of the time, without needing to even think about it.
However, in the described case, shared_ptr may indeed cause significant overhead, as shared_ptr guarantees thread-safety, which is in reality done using atomic increments/decrements, which necessarily has a much higher overhead for every assignment, temporary construction, and going out of scope. If you have only one thread, obviously thread-safety is something you don't need.

While the extra overhead is entirely negleglible most of the time (you normally don't create 20,000 temporary objects per second), it may nevertheless become an issue in combinations with containers, which may make a lot of copies and assignments, depending on what you do, and often at unpredictable times (resizing a vector, for example, would be horrendous).
Now, unluckily this is a kind of overly general statement which doesn't help much in your decision, but really the only way to find out is to benchmark.

Share this post


Link to post
Share on other sites
Ok thank you for bringing that up! That makes sense as far as considering efficiency goes! I also don't mean to fall into premature optimization.

The deeper issue I see however is in when to expose the shared_ptr object (or intrusive_pointer etc.) to external code (calling code, code outside of the owning object) and when / if it may be better to expose the heap object itself when ownership is clearly defined.
This may have implications which I want to understand right now or else later I will have to change the signature of many functions to work with object references / raw pointers instead of working with shared_ptr or similar.

After all I want to avoid to manually track objects and delete them but that doesn't necessarily mean I need to use the shared_ptr type everywhere if I can get away with using it just in the 'owning context'?
I am still wondering if that last assumption is a correct one.

Share this post


Link to post
Share on other sites

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