Skip to main content
gamedev.net
🔒 Locked

C++ object memory management, reference count or raw or mix?

Started by wqking 70 replies 23.9k views
Original Post
wqking
wqking
Three modes:
Raw means raw C++ new/delete pair.
Reference count means explicitly addRelease/subRelease (not any automatic share pointer in boost, the add/sub need to be called explicitly).
Mix means to use RC for shared object and raw for non-shared object.

I won't consider any auto garbage collection like management such as shared pointer. I would stick to manually manage the objects.

In a relative complex system, may include scene graph, even dispatch, resource management, what do you suggest to manage the objects?

The pros and cons of the three modes I can think:

Raw
Pros: every C++ developers know it very well.
Cons: hard for shared objects life time cycle management.

RC
Pros: Very good for shared object. Mac Cocoa uses it extensively.
Cons: What's the cons?

Mix
Pros: the most flexible
Cons: the newbie may get confused when to use Raw and when to use RC. And further more, messy up the system with mixed object management?

Looking forward to your opinion
Hodgman
Hodgman
Quote:
Original post by wqking
RC
Pros: Very good for shared object. Mac Cocoa uses it extensively.
Cons: What's the cons?
Cons:
Leads to memory-leaks or early-deletion bugs when add/sub aren't called the right number of times (raw-pointers also have this problem).

Also requires all of your classes that contain pointers to manually write destructors/copy-constructors/assignment-operators (raw-pointers also have this problem).


You can overcome both of these problems by using RAII to automatically call add/sub for you (just like shared_ptr does).
Quote:
Original post by wqking
I won't consider any auto garbage collection like management such as shared pointer. I would stick to manually manage the objects.
Shared pointer doesn't use anything fancy like garbage collection, it just uses reference counting.
wqking
wqking
Quote:
Original post by Hodgman
Quote:
Original post by wqking
RC
Pros: Very good for shared object. Mac Cocoa uses it extensively.
Cons: What's the cons?
Cons:
Leads to memory-leaks or early-deletion bugs when add/sub aren't called the right number of times (raw-pointers also have this problem).

Also requires all of your classes that contain shared pointers to manually write destructors/copy-constructors/assignment-operators (raw-pointers also have this problem).


You can overcome both of these problems by using RAII to automatically call add/sub for you (just like shared_ptr does).


Some strict defined rules for this kind of memory management may help to avoid memory leak/bad pointer. Cocoa did that.
I would try best to keep using reference/pointers rather than object copy because the code may be ported to Java in the future.

For RC indeed I personally don't like it very much but I found correctly using it may help much on shared objects.

Quote:
Shared pointer doesn't use anything fancy like garbage collection, it just uses reference counting.

I know. But the implicit operations (add/sub) in shared pointer makes me worry it maybe abused used. ;-)
thatguyfromthething
thatguyfromthething
Reference count in c++: cons are more than you can imagine if you don't keep it well defined and simple, and not too easy to track down. Reference count for every single thing is a disaster waiting to happen. If you keep it to just scene graph objects it might help you to be sure everything cleans itself, but your teammates might not enjoy stuff working in some odd manner either.

Mixed: well, would you really have every single thing reference counted? C++ is not what you want if so.

raw: Well, what's so hard, really? Objects in like a scene graph are very well defined as to when to get deleted or not so it's especially easy to check everything was deleted.
This is my thread. There are many threads like it, but this one is mine.
Decrius
Decrius
I'll tell you my experiences:

At one time, I did manual memory management, no RAII, no reference counting. It was al right back then, you just had to think memory stuff very well through. I had a number of memory leaks that were hard to track down, but the biggest con must be that I HAD to deep-copy everything. I couldn't share the pointer.

After adding reference counting, that saved me a lot of copying / memory, the code didn't change much. new still was new, deep-copy became increase_ref_count() often and delete became decrease_ref_count() || delete.

Still some memory leaks, reference count was sometimes too low or too high. I fixed them, but it was a pain in the ssA. For any new component I had to really think through every memory part.

Then I build my own smart pointer. After some testing (heavy testing, it's not trivial to make a smart pointer) it worked. After implementing I have never had ANY memory leak again from those parts.

There is some cons; you need to store a counter in each object or you must allocate it for each object and think well through to keep it consistent through all reference counters to the same object. So you have some more memory usage.

Another con is cross-linking. This one is important. In the case of cross-linking you must make one of both a regular (weak) pointer, and not use the smart-pointer. You must well track through whether this introduces leaks...

Good luck! My code got smaller and safer. No leaks, no code...haha :)
[size="2"]SignatureShuffle: [size="2"]Random signature images on fora
ChaosEngine
ChaosEngine
In C++, prefer RAII for managing any resource. Seriously, literally millions of man hours of experience have confirmed this as best practice in C++.

Use the stack if you can.
If you must use the heap, try to have clear ownership semantices (i.e. use scoped_ptr for single instances or boost.ptr_* for collections).
If ownership must be shared, use a shared_ptr.

Do not use a manual reference count, you will get it wrong. That's not a slight on you. Everyone, eventually gets it wrong.

The point here is why do you want to manage this manually? Unless you have a very good reason, you are simply making more work for yourself than necessary. Every minute you spend managing memory is a minute that could be spent making your game/app better.
if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight
thatguyfromthething
thatguyfromthething
Quote:
Original post by ChaosEngine
In C++, prefer RAII for managing any resource. Seriously, literally millions of man hours of experience have confirmed this as best practice in C++.

Use the stack if you can.
If you must use the heap, try to have clear ownership semantices (i.e. use scoped_ptr for single instances or boost.ptr_* for collections).
If ownership must be shared, use a shared_ptr.

Do not use a manual reference count, you will get it wrong. That's not a slight on you. Everyone, eventually gets it wrong.

The point here is why do you want to manage this manually? Unless you have a very good reason, you are simply making more work for yourself than necessary. Every minute you spend managing memory is a minute that could be spent making your game/app better.


RAII is just one of those meaningless things people say. If someone says manual you can be pretty sure that's a given. No one's really making objects that have some other object delete its members or anything like that. The issue here is not how the object/node cleans itself up, which however you do it is not usually in doubt about success except apparently to Bjarne Stroustroup. It's when you have an amorphous situation like a scene graph of graphics objects, how do you keep track of the damned things?

The possibilities are endless, and most of them have some degree of agony to bestow upon the end user. So once something not attached to some other node it disappears? Then we get stuff deleting itself behind our back, and stuff we still want to use has to be temporarily stuck to something for no apparent reason at all times. Is it all in some big container that tracks things and autodeletes? Then we close the app and deletion code that's already unloaded gets called. Do I have a smart pointer and some weak references? Well, that will fragment your heap to the moon and anyone but you using it will probably go through their days in constant doubt about what's going on.

This is my thread. There are many threads like it, but this one is mine.
Zahlman
Zahlman
Quote:
Original post by Decrius
deep-copy became increase_ref_count() often


!

When deep-copy can safely become increase_ref_count(), it means the deep-copy was unnecessary in the first place, and only being done to avoid a trickier memory management issue.

Every time I see clone() or Cloneable in Java code, I cringe, because it's a sign that someone is going out of his/her way to make the program inefficient because of a misunderstanding of reference semantics.

</rant>
Zahlman
Zahlman
Quote:
Original post by thatguyfromthething
RAII is just one of those meaningless things people say.


Absolutely not,

Quote:
The possibilities are endless....


and that's why.

The benefit of RAII is that it enforces a simple convention - "everyone cleans up their own mess" - with minimal effort on the part of the programmer. You never have to write code that finds all the nodes in a tree recursively and deletes them without running into dangling pointer issues - you just let the destructor operate recursively, and it just works.
Hodgman
Hodgman
Quote:
Original post by thatguyfromthething
...
What?
Decrius
Decrius
Quote:
Original post by Zahlman
Quote:
Original post by Decrius
deep-copy became increase_ref_count() often


!

When deep-copy can safely become increase_ref_count(), it means the deep-copy was unnecessary in the first place, and only being done to avoid a trickier memory management issue.


Yes! I didn't have any way to know how many objects use that instance. The only way to be sure in some way, was to deep-copy so only ever one object used the instance.

This was undesirable and caused a lot of unnecessary copies, so I fixed that with reference counting.

Quote:
Original post by thatguyfromthething
Do I have a smart pointer and some weak references? Well, that will fragment your heap to the moon and anyone but you using it will probably go through their days in constant doubt about what's going on.


Actually, smart pointers + weak pointers made my code SO much better, that people now actually DO know what's going on. And, my memory leaks are gone. Some stuff was really complex in my code, without RAII I would still be fixing those pesky bugs. Memory fragmentation? Memory pool!

Or did I just feed the troll...?
[size="2"]SignatureShuffle: [size="2"]Random signature images on fora
Treb
Treb
You didn't include handles. Most of the time they are used to achieve garbage collecting, but they can be used for other areas as well.

I use handles for game objects. I could have every GameObject be a weak_ptr with one shared_ptr in the GameObjectManager. Then when I wanted to destroy the object the shared_ptr would be removed deleting the object. But imo this is just tedious, that strategy was used on my last project and I didn't like it.

Then I use ref counting for resources so I can share a texture and know when everyone is done using it.

Outside of this I have found little use for ref counting. I use boost::scoped_ptr and std::auto_ptr for RAII, but I rarely encounter a need for boost::shared_ptr.

I would likely fall under Mix.
Quote:
the newbie may get confused when to use Raw and when to use RC.

The only con here is that there is a newbie programming. Making code overly protective from inexperienced programmers only makes it convoluted and illegible, which doesn't help them (speaking from experience).

Quote:
And further more, messy up the system with mixed object management?

Maybe, but like containers there is no single management solution for all situations.
mazathome
mazathome
Hi,

lots of delete's spread all over the code are ugly. But using reference counted containers is overhead and is not nice to read because method signatures expand from doSmth(Object *o1, Object *o2, Object *o3) to doSmth(WeakPointer<Object> o1, WeakPointer<Object> o2, WeakPointer<Object> o3).

For my current hobby project i have introduced something like Qt's QObject. It's a custom Object class (that derives from classes NodeContainer<T> and NodeChild<T>)
and allows me to "pin" objects to other objects. In my code i am able to use raw pointers. If a parent object is deleted all child objects are also deleted.

Example:
ISocket *EpollSocketLoop::createSocket(){    return new EpollSocket(this);}


One might delete the returned object (socket) but the object is always deleted, if the socket loop is deleted.

Captain P
Captain P
Quote:
Original post by thatguyfromthething
It's when you have an amorphous situation like a scene graph of graphics objects, how do you keep track of the damned things?

RAII means that each object cleans up it's own mess. What you're talking about here is a matter of ownership: who owns what?

Typically, the object that owns the graphics object should retain ownership. If it goes down, so does it's graphics object. If that one is still in the scene graph, it can notify it's parent/children from it's destructor. The alternatives are to explicitly pass ownership to the scene graph, or to share ownership (typically by using shared pointers). Obviously, if you pass on ownership, you should no longer maintain a reference to such an object - it's lifetime is no longer under your control.

So yeah, it's just a matter of ownership. Once that's decided, it's pretty easy.


Quote:
Original post by mazathome
But using reference counted containers is overhead and is not nice to read because method signatures expand from doSmth(Object *o1, Object *o2, Object *o3) to doSmth(WeakPointer<Object> o1, WeakPointer<Object> o2, WeakPointer<Object> o3).

Using typedefs should help keeping things readable. As for overhead... how does your own solution compare? I mean, you're doing bookkeeping just as well...
phresnel
phresnel
Quote:
Original post by mazathome
not nice to read because method signatures expand from doSmth(Object *o1, Object *o2, Object *o3) to doSmth(WeakPointer<Object> o1, WeakPointer<Object> o2, WeakPointer<Object> o3).


a) You'll get used to it.
b) typedef
c) On the other hand ...

struct Foo : noncopyable {        Foo () : alpha(0), omega(0) {        }        ~Foo() {                if (0 != alpha) delete alpha;                if (0 != omega) delete omega;        }private:        Bar *alpha, *omega;};


vs.

struct Foo : noncopyable  {private:        scoped_ptr<Bar> alpha, omega;};


Readability (in the last example, you could even skip the derivation from noncopyable-mixin, as scoped_ptr<> is noncopyable, too) :)
Zahlman
Zahlman
Quote:
Original post by mazathome
In my code i am able to use raw pointers.


You say that like smart pointers were hard to use or something.
iMalc
iMalc
Quote:
Original post by phresnel
Quote:
Original post by mazathome
not nice to read because method signatures expand from doSmth(Object *o1, Object *o2, Object *o3) to doSmth(WeakPointer<Object> o1, WeakPointer<Object> o2, WeakPointer<Object> o3).


a) You'll get used to it.
b) typedef
c) On the other hand ...

*** Source Snippet Removed ***

vs.

*** Source Snippet Removed ***

Readability (in the last example, you could even skip the derivation from noncopyable-mixin, as scoped_ptr<> is noncopyable, too) :)
You do know that checking for NULL (or zero) pointers before calling delete is reundant right? 'delete' already does that internally, which is guaranteed by the standard.
However, to furthur illustrate the size diffference between the two examples, you should add code preventing copy construction or assignment to the top example, by declaring them private and leaving them unimplemented.
For some time now I've considered a type which fails to copy correctly yet does not disable copying, to be fundamentally broken, i.e. a bug.
diablos_blade
diablos_blade
Quote:
Original post by iMalc
However, to furthur illustrate the size diffference between the two examples, you should add code preventing copy construction or assignment to the top example, by declaring them private and leaving them unimplemented.
For some time now I've considered a type which fails to copy correctly yet does not disable copying, to be fundamentally broken, i.e. a bug.


That is what the inheritence from noncopyable is for. Assuming it is equivilent to the boost implementation, it declares a private assignment operator and copy constructor with no definition. Causing a compiler error if anyone attempts to copy a child class.
Christian Weis
Christian Weis
Why not writing a simple mark&sweep GC?! This algorithm is safe, reliable, and not that inefficient as many people think. I use mark&sweep for more than 7 years now in my projects and dit not have any problems with it yet. Give it a try.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.