[see also AlgorithmsThatDemandGarbageCollection
went crazy trying to implement MoneyObject
in C++. The structure of the two is very similar in some ways. The lack of GarbageCollection
really complicates things. Is this true of all ValueObject
implementations in C++?
Yes.. the real issue was the interaction between Money and MoneyBag
. When a method accepts an object and either returns that object or a new one you've got to make some choices. I tried making a "smart pointer template" but I discovered that I was having trouble both keeping it general and accommodating inheritance. Also, I discovered strange problems when I attempted to use it in an STL map. Looking back on it, I'm sure that I could have used one of Cope's idioms, but I was not keen on increasing the code complexity to the point where it distracted from the code's purpose: to be an example of a class under test. So, I dropped that example and made up another one.
It was a real eye-opener. Afterward, I posted to comp.object: Anyone who thinks that C++ is the "be all and end all" should download JUnit [JavaUnit]
and try to port MoneyObject
maintaining all the functionality and extensibility of the original.
I don't tend to be a language zealot. Each tool has its place. But, I do think that it would be neat to write an article that (1) explains MoneyObject
and shows the Java code (explaining the signatures needed for DoubleDispatch
and what has to happen in inheritance) and (2) shows what a C++ implementation would have to do to approach the original and (3) shows the same thing in Smalltalk. It would flow like a piece of music.
immutable? Was there a problem apart from memory management? -- DaveHarris
It was predominantly memory management. I'm not sure yet that it was the ValueObject
-ness.. It does occur to me, abstractly, that when identity does not matter and an object can return any ValueObject
from a method as long as it has the "right" value, it is easier in a garbage-collected language to play fast and loose and do things that are hard to map to non-GC languages. That could be the case with MoneyObject
. I'll have to retrace my steps. It would be a fun thing to figure out. -- MichaelFeathers
As support for the last comment, I can tell you that Nextstep programming got
a zillion times easier when reference counting was added to the frameworks. The basic construct was very simple: a new class, NSAutoReleasePool was added
to the libraries. And, since everything in ObjectiveCee
descended from Object, Object was rewritten to interact with instances of NSAutoReleasePool. When you created an object, it got a ref count of 1. When you were done with it, you decremented the ref count. Etcetera.
Ref counts were resolved in the event handling mechanism. E.g. if it was time for a UI event to be dispatched, the pool would first clean up all the objects that had a zero ref count.
I know: it's pretty much a brain dead mechanism. Me, I wouldn't have dreamed of implementing something this ugly. But, and here's the interesting (almost extreme) point: it was incredibly simple and it worked quite well.
All of a sudden, almost overnight, I began to freely create ValueObject
classes and start passing them around willy-nilly. And all this code I used to write to handle object lifetimes just evaporated. In regards to your suspicion up above - having some sort of GC mechanism does change your code dramatically and does orient you in the direction of ValueObject
(at least, it did me).
NSAutoReleasePool is also, in my mind, a great example of doing the simplest thing that could possibly work. The people who built it retrofitted a very simple GC mechanism onto an existing framework and, in the process, simplified programming immensely.
Not sure I understand how you did this, William. When assigning a new ValueObject over an old one, did you have to always decrement the count, or ?? -- RonJeffries
I'm not sure what assign a new value object over an old one
means. You did have to manually increment the ref count when you wanted to retain an object into the next event cycle. And you did have to manually decrement the count when you were done with an object (assuming you incremented the reference count).
Not as cumbersome as it seems. The guarantee that nothing would be collected until the next event was processed meant that you could return a ValueObject
, it would get passed around the bowels of your program and very few objects would bother to increment or decrement the reference count (only those ones that wanted to keep it for a while).
Like I said, pretty much a brain-dead retrofitting of a gc-like mechanism. The amazing thing, to me, was that it worked so well. If I'd been at Next, and been told to retrofit GC into ObjectiveCee
, I'd probably still be there, knee deep in the theory.
Yes. Some tasks demand GC. If it isn't in your language, you have to craft it by hand.
I've had a look at MoneyObject
now; I think it flows pretty well in Java, too. Java has more type declarations, but if you use Java you presumably believe manifest types are a benefit.
I did notice that you used DoubleDispatch
. This meant the base interface had to have an addSubclass() method for every subclass. The alternative would be to use instanceof and casting. Both are forms of RunTimeTypeInformation
, but one requires more static declarations and delivers correspondingly more static safety. If you use UnitTest
s for your safety, maybe instanceof would be more appropriate?
I have experience with Lisp, Smalltalk, Prolog, Java, and other languages featuring automatic GarbageCollection
I have also done years of work with C and C++.
Value objects do not require garbage collection.
In C++, if your ValueObject
s have particularly costly state, as do Strings, you'd be wise to use the handle-body idiom, with reference counting.
s are values. You should be using them as if they were native types.
That means, in C++, that you shouldn't be passing around pointers or references to ValueObject
In C++, you don't work with pointers to ValueObject
s for the same reason you don't work with pointers to integer values:
The value of the object is the only thing that matters; the object itself has no unique entity.
The "5" you have stored over there is just as good as the "5" you have stored over here, and it doesn't matter if they reside at the same address or not; if you add 3 to one, you get a new value. (Value objects are immutable.)
So, when you pass a ValueObject
into or out of a function, or put it into a collection, you should use the type of the object itself; not a pointer or reference to it.
That means that you will be making heavy use of the copy constructor and destructor of your object.
The compiler will introduce (and usually optimize away) quite a few hidden "temporary objects".
[I hope my words are more helpful than harsh. Have a good evening.]
Very well said. This matches my experience. I've done lots of value objects in C++, always by defining copy semantics for the class.
C++'s not very well disguised emphasis upon efficiency leads to people getting crazy with pointers and references when a simple object copy would lead to much simpler code. -- WayneConrad
If the ValueObject
is large, copying the object is a significant overhead compared to passing around a pointer to the object. However, because ValueObjectsShouldBeImmutable
, reference counting can be used to automate memory management - It is impossible to create circular references between ValueObject
s. Reference counting can be encapsulated in one or two template classes. -- NatPryce
Also true if the ValueObject has variable size, such as a string. Such a thing cannot be passed around by value directly (in particular, returned from a function directly) because the compiler knows not how much stack to allocate for it. -- ScottJohnson
I can, on zero hands, count the number of times that I had to complexify a C++ value object to meet performance constraints. Of course, I never ended up with very big value objects (I get too confused if an object has very much stuff in it). -- WayneConrad
Modern memory hierarchies (locality) dominate all considerations of performance. The size of an object is negligible until you get to a significant multiple of cache lines, and of course "significant" varies (my little cop-out!). This is a classic example of premature optimization -- a developer shouldn't bother "optimizing" an object as small as MoneyObject
without bothering to 1) measure it with a profiler, 2) consider the usage context. In nearly all cases like this, the right thing to do is to start with the brute force assumption. Of course, environments that have inherent value types (C++, CLR, etc.) have it easier than languages like Java, where the environment forces you to make stupid, useless trade-offs (mutable and reference management, or immutable?). -- ArlieDavis