9

From what I can see, there are two pervasive forms of resource-management: deterministic destruction and explicit. Examples of the former would be C++ destructors and smart pointers or Perl's DESTROY sub, whilst an example of the latter would be Ruby's blocks-to-manage-resources paradigm or .NET's IDispose interface.

Newer languages seem to opt for the latter, perhaps as a side-effect of using garbage collection systems of the non-reference-counting variety.

My question is this: given that destructors for smart pointers or reference-counting garbage collection systems -- almost being the same thing -- allow implicit and transparent resource destruction, is it a less leaky abstraction than the non-deterministic types which rely on explicit notation?

I'll give a concrete example. If you have three C++ subclasses of a single superclass, one may have an implementation that doesn't need any specific destruction. Perhaps its does its magic in another way. The fact that it doesn't need any special destruction is irrelevant -- all of the subclasses are still used in the same way.

Another example uses Ruby blocks. Two subclasses need to free resources, so the superclass opts for an interface that uses a block in the constructor, even though other specific subclasses might not need it since they require no special destruction.

Is it the case that the latter leaks implementation details of the resource destruction, whilst the former does not?

EDIT: Comparing, let's say, Ruby to Perl might be more fair since one has deterministic destruction and the other hasn't, yet they're both garbage-collected.

Louis Jackman
  • 243
  • 1
  • 5
  • 5
    I am tempted to say "yes", but I like to hear what others have to say on this. – Bart van Ingen Schenau Mar 12 '13 at 07:34
  • Transparent resource destruction? Apart from the fact that you have to use smart pointers instead of normal pointers? I do not think this is more transparent than having just one mechanism (references) to access objects (in C++ you have at least four or five). – Giorgio Mar 12 '13 at 08:39
  • @Giorgio: "Ways to access an object" is quite vague. Do you mean read or write? Const/Volatile qualification? Pointers are not really "a way to access an object"; pretty much any expression results in an object and dereferencing a pointer just isn't that special. – MSalters Mar 12 '13 at 10:37
  • 1
    @Giorgio: In that OOP sense, you can't send a message to a C++ pointer. You need to dereference the pointer to send the message `(*ptr).Message()` or equivalently `ptr->Message()`. There's an infinite set of allowed expressions, as `((*ptr))->Message` is also equivalent. But they all boil down to `expressionIdentifyingAnObject.Message()` – MSalters Mar 12 '13 at 10:48
  • @MSalters: In OOP there is only one way to interact with an object: sending it a message. In order to send an object a message you need to reference it (access was meant in this sense: getting hold of) in some way (to pass the object itself as an argument to the method you want to call). In C++, using pointers is one of the possibilities. – Giorgio Mar 12 '13 at 10:49
  • "But they all boil down to expressionIdentifyingAnObject.Message()": If resource management was done transparently, this expression (and the expression that you use to create objects) should not reflect how the object lifetime is managed (stack object, raw pointer, smart pointer). Transparent means that you do not see it. – Giorgio Mar 12 '13 at 10:53
  • Hmm, perhaps using C++ as the example of deterministic resource management was an error on my behalf due to its low-level nature. How about Perl 5? It's fully garbage-collected, yet has deterministic resource management due to reference counting. (That also leads to circular references, but that's another discussion for another day.) – Louis Jackman Mar 12 '13 at 15:44
  • 1
    With refcounting you need to be careful about avoiding circles. So that abstraction leaks as well, just in a different way. – CodesInChaos Mar 12 '13 at 16:43
  • @CodesInChaos: Languages can prohibit cycles by design in order to make reference counting accurate. Erlang and Mathematica do, for example. But not C++, of course. :-) – J D May 31 '15 at 16:59

4 Answers4

2

Your own example answers the question. The transparent destruction is clearly less leaky than explicit destruction. It can leak, but is less leaky.

Explicit destruction is analogous to malloc/free in C with all the pitfalls. Maybe with some syntactic sugar to make it appear scope-based.

Some of the benefits of transparent destruction over explicit:
--same usage pattern
--you can't forget to release the resource.
--clean up details do not litter the landscape at the point of usage.

mike30
  • 2,788
  • 2
  • 16
  • 19
2

The failure in the abstraction is actually not the fact that garbage-collection is non-deterministic, but rather in the idea that objects are "interested" in things that they hold references to, and are not interested in things to which they do not hold references. To see why, consider the scenario of an object which maintains a counter of how often a particular control is painted. On creation, it subscribes to the control's "paint" event, and on disposal it unsubscribes. The click event simply increments a field, and a method getTotalClicks() returns the value of that field.

When the counter object is created, it must cause a reference to itself to be stored within the control it's monitoring. The control really doesn't care about the counter object, and would be just as happy if the counter object, and the reference to it, ceased to exist, but as long as the reference does exist it will call that object's event handler every time it paints itself. This action is totally useless to the control, but would be useful to anyone who would ever call getTotalClicks() on the object.

If e.g. a method were to create a new "paint-counter" object, perform some action on the control, observe how many times the control was repainted, and then abandon the paint-counter object, the object would remain subscribed to the event even though nobody would ever care if the object and all references to it simply vanished. The objects would not become eligible for collection, however, until the control itself is. If the method were one that would be invoked many thousands of times within the control's lifetime [a plausible scenario], it could cause a memory overflow but for the fact that the cost of N invocations would likely be O(N^2) or O(N^3) unless subscription-processing was very efficient and most operations didn't actually involve any painting.

This particular scenario could be handled by giving having the control keep a weak reference to the counter object rather than a strong one. A weak-subscription model is helpful, but doesn't work in the general case. Suppose that instead of wanting to have an object which monitors a single kind of event from a single control, one wanted to have an event-logger object which monitored several controls, and the system's event-handling mechanism was such that each control needed a reference to a different event-logger object. In that case, the object linking a control to the event logger should remain alive only as long as both the control being monitored and the event logger remain useful. If neither the control nor the event logger holds a strong reference to the linking event, it will cease to exist even though it's still "useful". If either holds a strong event, the lifetime of the linking object may be uselessly extended even if the other one dies.

If no reference to an object exists anywhere in the universe, the object may safely be considered useless and eliminated from existence. The fact that a reference exists to an object, however, does not imply that the object is "useful". In many cases, the actual usefulness of objects will depend upon the existence of references to other objects which--from the GC perspective--are totally unrelated to them.

If objects are deterministically notified when nobody is interested in them, they will be able to use that information to make sure that anyone who would benefit from that knowledge is informed. In the absence of such notification, however, there is no general way to determine what objects are considered "useful" if one knows only the set of references which exist, and not the semantic meaning attached to those references. Thus, any model which assumes that the existence or non-existence of references is sufficient for automated resource management would be doomed even if the GC could instantly detect object abandonment.

supercat
  • 8,335
  • 22
  • 28
0

No" the destructor, or other interface that says "this class must be destroyed" is a contract of that interface. If you make a subtype that doesn't require special destruction I would be inclined to consider that a violation of the Liskov Substitution Principle.

As for C++ vs. others, there's not much difference. C++ forces that interface on all it's objects. Abstractions can't leak when they're required by the language.

Telastyn
  • 108,850
  • 29
  • 239
  • 365
  • 4
    "If you make a subtype that doesn't require special destruction" That's not a LSP violation, since no-op is a valid special case of destruction. The problem is when you add the destruction requirement to a derived class. – CodesInChaos Mar 12 '13 at 16:44
  • I'm getting confused here. If one needs to add special destruction code to a C++ subclass, it doesn't change its usage patterns at all, because it's automatic. That means the superclass and subclass can still be used interchangeably. But with explicit notation for resource-management, a subclass needing explicit destruction would make its usage incompatible with a superclass, wouldn't it? (Assuming the superclass didn't need explicit destruction.) – Louis Jackman Mar 12 '13 at 17:19
  • @CodesInChaos - ah yes, I suppose that's true. – Telastyn Mar 12 '13 at 18:11
  • @ljackman: A class which requires special destruction imposes a burden on whoever calls its constructor to ensure that it gets carried out. This does not create an LSP violation since a `DerivedFooThatRequiresSpecialDestruction` can only be created by code which calls `new DerivedFooThatRequiresSpecialDestruction()`. On the other hand, a factory method which returned a `DerivedFooThatRequiresSpecialDestruction` to code that wasn't expecting something requiring destruction, would be an LSP violation. – supercat Feb 18 '14 at 00:09
0

My question is this: given that destructors for smart pointers or reference-counting garbage collection systems -- almost being the same thing -- allow implicit and transparent resource destruction, is it a less leaky abstraction than the non-deterministic types which rely on explicit notation?

Having to watch for cycles by hand is neither implicit nor transparent. The only exception is a reference counting system with a language that prohibits cycles by design. Erlang might be an example of such a system.

So both approaches leak. The main difference is that destructors leak everywhere in C++ but IDispose is very rare on .NET.

J D
  • 2,332
  • 24
  • 17
  • 1
    Except cycles are exceedingly rare and practically never occur except in data structures that are explicitly cyclic. The main difference is that destructors in C++ are handled properly everywhere but IDispose rarely handles the problem in .NET. – DeadMG May 31 '15 at 09:36
  • "Except cycles are exceedingly rare". In modern languages? I would challenge that hypothesis. – J D Jun 01 '15 at 08:39