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

Started by
22 comments, last by dmatter 10 years, 8 months ago
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.
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.

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.

http://tinyurl.com/shewonyay - Thanks so much for those who voted on my GF's Competition Cosplay Entry for Cosplayzine. She won! I owe you all beers :)

Mutiny - Open-source C++ Unity re-implementation.
Defile of Eden 2 - FreeBSD and OpenBSD binaries of our latest game.


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.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]


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.


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.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

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.

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.

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.

SlimDX | Ventspace Blog | Twitter | Diverse teams make better games. I am currently hiring capable C++ engine developers in Baltimore, MD.


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.

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

This topic is closed to new replies.

Advertisement