So maybe this is some sort of selective perception thing but I'm getting somewhat annoyed that software design articles/discussions and such seem to be mostly about what not to do. Were I to follow all the "advice" about bad practice I wouldn't write code at all.
So I was reading the "Demise of the game entity"-thread and while I agree with the problems described there and encountered them myself, no one is actually suggesting concrete solutions. Then I went on to read the articles the thread references to and right at the beginning I find this:
When did allocating memory in a constructor become a bad idea?
I have encountered some architecture’s that have banned the use of Constructors and Destructors in game entities! Usually because of a misunderstanding of how C++ works and usually backed up by the fact that some engineers just can’t stop themselves from doing memory allocations and other nonsense in the constructor.
I always feel stupid when I encounter this kind of statement, since the apparently competent authors are dropping these things like it was completely obvious that this is nonsense, but it isn't to me. Does that mean I'm a horrible programmer? And again, I'm told what not to do, but not what I should do...
(sorry, if this is kinda rantish)
You hit upon two pervasive issues of confusion.
The first is that constructive criticism is hard work - I would hope that most articles say don't do it this way or that way and then present an acceptable, ideal, or optimized method.
e.g. It's easy to say some aspect of a design is undesirable. It is quite difficult to provide a universally better approach. Adding complexity to address rare conditions or marginal concerns is not a good idea.
The more specific issue about memory allocation in constructors is mostly about determinism (which is a lacking quality in software today).
The difficulties of testing it is rooted in the lack of determinism.
Consider this contrived example,
all_good = new int;
death_to_all = new int[1<<31];
If the ctor throws... the dtor is not
I think the only way to handle this is zero all members prior to performing any allocations (inject determinism, we now know they are all zero prior to the possibility of an exception thrown), then catch any exception thrown, perform clean-up on non-null members, then rethrow the exception. (This is a pain-in-the-ass so no one does this.)
If you start new'ing in the ctor you may become tempted to call other functions in the ctor. Perhaps even a virtual function in the ctor which will not invoke the current class's implementation but, rather, the base-class implementation because 'this' class does not exist until the ctor successfully completes (so the vtable cannot be trusted). In C++ I believe this is technically undefined behavior, there might not be
a base-class implementation. I would assume in Java and certainly in C#/.Net there are additional keywords that clarify and eliminate this issue (you get a compiler error instead of crashing at run-time.)
The more general problem this touches on is one I call "granularity" (it also touches "Conceptual Integrity" but everything touches CI).
Our Conceptual Integrity goal is to make our entire program adhere to RAII without requiring the programmer to resort to extraneous tactics such as a two-staged atomic object initialization consisting of a zeroing phase followed by a allocation phase combined with lots of catch/rethrow blocks to clean everything up.
To have good CI it has to be straight-forward.
What we need is a consistent rule that we can follow that will produce correct code.
In C++ there is a concept known as RAII (resource-acquisition-is-initialization).
An example simple rule is you are not allowed to use 'new' in a constructor unless you are creating a primitive object that contains one, and only one, dynamic data member.
Primitive objects must be RAII compliant.
All other objects are now composed of primitive objects which automatically makes them RAII compliant.
e.g. replace new int with a vector.resize().
Another simple rule is object-model classes (classes part of your big-picture design) are not allowed to use RAII.
Only low-level utility classes are allowed to use RAII.
(Why I call it a granularity issue.)
Or even, Only use RAII if there is no other choice.
For tasks such as [exception] safely acquire and hold a mutex, this is the "only" choice.
- The trade-off between price and quality does not exist in Japan. Rather, the idea that high quality brings on cost reduction is widely accepted.-- Tajima & Matsubara