Jump to content
  • Advertisement
Sign in to follow this  
Grain

Do you find C#'s lack of an explicit destructor to be an issue?

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

The only time this has been an issue for me was when a different team at work was writing their C#-to-C interop layer and decided it would be a good idea for their interop functions to return C pointers to C#, then use C# finalizers to free the C memory.

This is a bad idea in general, but it would technically work, say, on a desktop PC. The problem was that we were running on an iPhone with extremely limited available RAM. The managed heap and C heap are separate, and the C heap filled up and crashed with an out-of-memory before the C# allocations triggered the GC to execute any finalizers. Whoops.

Due to the way the interop layer was written, IDisposable would have just made the C# side of things excessively hard to manage. The fix was to copy the data over to C# and immediately free the C memory. This caused the desire behavior of the GC kicking in when necessary.



This is a perfect example of what swiftcoder is talking about - When using a managed language, you need to stop trying to manage your own memory. You should insulate as much of the managed layer from unmanaged resource management by enforcing managed behavior as close to the unmanaged code as possible to prevent resource management code from polluting your managed code.

Share this post


Link to post
Share on other sites
Advertisement

There is no particular reason why developers should have to explicitly manage the lifetime of resources, just as they no longer explicitly manage the lifetime of memory.

I agree that in a managed language, you need to use a different mindset than you do in C/C++/etc, however, I've had a bunch of bugs in my C# tool chain from being lazy with resource lifetimes.
 
e.g. when running my asset build tool, at one point, a file is opened for writing and asset data is written into it. Later on that same file is opened for reading, as another asset is dependent on that data.
If I just open up some streams and trust the GC to clean up my resources, the second open-for-read operation quite often fails, because the open-for-write resource handle hasn't been cleaned up yet. This forces me to either use C#'s using blocks, or to write C-style cleanup code (with the modern twist of doing it in finally blocks, etc). When there are actually requirements on the lifetime of resources, relying on the GC's arbitrary lifetime management doesn't work :/

Lets say there's a basic game entity class, and among it's members is a sprite object. Now there is also a graphics manager class that simply cycles through all existing sprites and draws them to the screen. When an entity is created it hands the graphics manager a reference to it's sprite so it can draw it. Now what happens when that game entity dies. Well lets amuse it's kept track of by a world manager or something like that that does game logic. The the world manager checks if the entity has hit 0 hp and the manager removes it from it's list of living entity and it eventually gets eaten by GC.

It's a pretty small change to fix that, from:
m_worldManagerEntities.Remove(sprite);
to:
sprite.Dispose(); // you're dead now, clean up please
m_worldManagerEntities.Remove(sprite);
I personally wouldn't use a design like this in C# or C++ w/ RAII though. You're making the assumption that by removing the sprite from this list, that it will stop being drawn... I'd much prefer this chain-reaction to be stated explicitly, rather than the unwritten assumption that it will just happen by magic.

Now remember that the graphics manager still has a reference to it's sprite, so GC will never touch it.

If you were writing this in C++ with RAII smart pointers, you'd have the exact same problem. You'd solve that problem in both C++ and C# by having your graphics manager use a weak reference. Edited by Hodgman

Share this post


Link to post
Share on other sites

This is incorrect. The finalizer for a class whose constructor throws an exception will get called. This is because an object has been allocated (regardless of whether the constructor finishes or not) that will, due to the exception, not have any references to it. The garbage collector will see that and call the finalizer.
 

 

Thats true but check again. This class has a thread running in it. The Finalizer will not be called.

Edited by Karsten_

Share this post


Link to post
Share on other sites


I agree that in a managed language, you need to use a different mindset than you do in C/C++/etc, however, I've had a bunch of bugs in my C# tool chain from being lazy with resource lifetimes.

I'm not saying be lazy, I'm saying do it the right way.

 

Even in C++, people get this wrong a lot of the time. Abusing shared_ptr because you haven't explicitly defined resource lifetimes, is pretty much evil.

Share this post


Link to post
Share on other sites

 


I agree that in a managed language, you need to use a different mindset than you do in C/C++/etc, however, I've had a bunch of bugs in my C# tool chain from being lazy with resource lifetimes.

I'm not saying be lazy, I'm saying do it the right way.

 

Even in C++, people get this wrong a lot of the time. Abusing shared_ptr because you haven't explicitly defined resource lifetimes, is pretty much evil.

 

But I DO explicitly defined resource lifetimes. The struggle is to wrestle with the mechanism by which you get the rest of the code to acknowledge an object is dead. My point is that In a managed language like C# there is extra implementation to be done. 

Share this post


Link to post
Share on other sites


But I DO explicitly defined resource lifetimes. The struggle is to wrestle with the mechanism by which you get the rest of the code to acknowledge an object is dead.

Either you haven't explicitly defined lifetimes, or you haven't explicitly defined ownership (which goes back to my earlier point, that "shared" is not a valid definition of ownership).

 

If both are explicitly defined, then there cannot be any other code that refers to dead resources.

 

My point is that In a managed language like C# there is extra implementation to be done.

And my point is that you should be doing this implementation in C++, too. Skating by with shared ownership semantics only takes you so far.

Share this post


Link to post
Share on other sites
This is part of the switch to almost every other language, not just C#.


Many C++-only developers have developed the notion that somehow cleaning up an object and releasing resources are synonymous with destroying an object.

They are not the same.

Many c++ developers simply delete objects without bothering to close them, because they know that the destructor will call the cleanup code if they didn't. That isn't to say most objects are missing cleanup calls; you can close files, release resources, and otherwise clean up objects without destroying them.

Just because c++ destructors also tend to include cleanup code does not mean that cleanup code should never be called elsewhere. Objects should be cleaned up when you are done with them; destruction should clean them up as a failsafe.

It is a bad habit that c++-only programmers tend to get into. Programmers who are used to a wider range of languages generally don't get stuck in that rut.

Share this post


Link to post
Share on other sites

I agree that in a managed language, you need to use a different mindset than you do in C/C++/etc, however, I've had a bunch of bugs in my C# tool chain from being lazy with resource lifetimes.

I'm not saying be lazy, I'm saying do it the right way.
If you've got any suggestions for my use case, I'd love to improve my C# code...
n.b. My example was responding to this:

There is no particular reason why developers should have to explicitly manage the lifetime of resources, just as they no longer explicitly manage the lifetime of memory.


In my build system (like 'make', etc), when building a target, I acquire a variable number of file handles, some read only and some write only, and store them in two lists. I then run the build operation using those two lists, and afterwards have to manually close all those file handles.
This is 'explicitly managing resource lifetimes in a managed language', which apparently is bad (it looks like a C algorithm). In C++ I would set thing up so that when m lists go out of scope, all the handles would be closed.

Share this post


Link to post
Share on other sites

Swiftcoder's words are far more critical than many people are willing to hear. I used to struggle with C#'s GC behavior a little bit, because it wasn't RAII the way C++ has. But you know what? It's RAII I gave up on. Managing data/memory lifetimes in larger, more coherent blocks with very explicitly designed ownership dispenses with the big problems here, relegating the details to just that, details. Does it work for everything? Not at all. But core game systems work on very rigid, well defined lifetimes. Understand how and when your objects are being used, and make that part of your primary design criteria. Don't vomit GC objects or shared_ptr objects because you have ill defined boundaries of ownership and lifetime.

Edited by Promit

Share this post


Link to post
Share on other sites

 


But I DO explicitly defined resource lifetimes. The struggle is to wrestle with the mechanism by which you get the rest of the code to acknowledge an object is dead.

Either you haven't explicitly defined lifetimes, or you haven't explicitly defined ownership (which goes back to my earlier point, that "shared" is not a valid definition of ownership).

 

If both are explicitly defined, then there cannot be any other code that refers to dead resources.

 

 

 

My point is that In a managed language like C# there is extra implementation to be done.

And my point is that you should be doing this implementation in C++, too. Skating by with shared ownership semantics only takes you so far.

 

Ok. Object 1 owns object 2. This is explicit. Object 3 has a reference to object 2 because it needs to work with some data it has. Object 1 dies, so it kills object 2 as well.  This is also explicit. 

 

Nowwe still need to inform object 3 that it's reference to object 2 is no longer valid. 

Edited by Grain

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.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!