Why am I only told what not to do

Started by
13 comments, last by Codarki 12 years, 7 months ago

As evidence of how implausible it is that this is good advice, many objects that are part of the standard library (std::string, std::vector...) do provide constructors that allocate memory.


Standard library treats memory allocation as orthogonal by using allocator concept. Entire operation of the library handles all resource allocations via external interface, which falls in line with original advice. While operator new can be overloaded, the default versions do not pass in context in which they are called, suffering from typical global namespace issues.

When designing APIs, the biggest mistake is to internalize resource allocation. In C, allocator function should be provided. In C++, allocator. In Java or C#, factory interface. Dynamic languages do not suffer from this as much since any call can be mixed-in or modified at any point to insert cross-cut functionality.


The lesson here is rarely emphasized enough, but resource life cycle is absurdly complicated problem. By including it into some library or algorithm it creates such rigid design that it may be rendered useless for any further development, either through reuse or through feature changes. Resources here can range from memory to files but even to number of rows in database, number of URLs supported or lines in a file.

FactoryStrategyFactorySingletonProvider is somewhat of a joke in Java world. Admittedly, Java is a clumsy and verbose language, but the problem is real. Di/IoC is in many ways the worst of all worlds, but it emerged as only scalable (in terms of development) methodology. Key lesson is to decouple resource creation/allocation from business logic/rule engine. It really is that important as projects grow.

And as said, dynamic languages do not suffer from this problem, even though at surface they appear to be allocating objects left and right. Focus is on resource allocations and memory in most languages and problems isn't important enough. C and C++ tend to be used precisely for this reason, so memory should be treated as one of resources that needs to carefully managed.
Advertisement

Hi,

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:
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.

When did allocating memory in a constructor become a bad idea?

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)
[/quote]

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,

class Exploder
{
int* all_good;
int* death_to_all;
public:
Exploder()
{
all_good = new int[1000];
death_to_all = new int[1<<31];
}
}


If the ctor throws... the dtor is not invoked.
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

[quote name='japro' timestamp='1312584860' post='4845260']
Hi,

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:
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.

When did allocating memory in a constructor become a bad idea?

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)
[/quote]

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,

class Exploder
{
int* all_good;
int* death_to_all;
public:
Exploder()
{
all_good = new int[1000];
death_to_all = new int[1<<31];
}
}


If the ctor throws... the dtor is not invoked.
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.
[/quote]
- 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

If the ctor throws... the dtor is not invoked.
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.)


You got the C# part wrong there is no keyword to do this in C#, it allows you to call virtual functions from the constructor. C# constructs from the derived type downwards so the vtable exists as soon as you hit the constructor body.

And to extend your point about if the constructor throws the destructor is not invoke this is valid for any failing constructor regardless of it throwing or not, it it fails it will not call it's destructor. You have just created a zombie object that will keep up taking memory but won't ever be released until program termination. And one of the easiest ways to generate this is whit allocations in the constructor, thats the reason why people insist on banning allocations in the constructors.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

So basically the article suggests using a bool sometype::init() instead of the constructor object for allocating resources as init can fail gracefully and constructors will just create massive mem leaks. I actually agree 100% with the article on this issue and would like to suggest http://www.scs.stanford.edu/~dm/home/papers/c++-new.html as a good read on the subject of constructors

So basically the article suggests using a bool sometype::init() instead of the constructor object for allocating resources as init can fail gracefully and constructors will just create massive mem leaks. I actually agree 100% with the article on this issue and would like to suggest http://www.scs.stanf...rs/c++-new.html as a good read on the subject of constructors

I think multi-step construction has too many disadvantages. If you really need complex building, maybe builder pattern could do the job. Pass the builder object around until the real object can be safely constructed.

That rant you linked is wrong on so many levels I'd "unlike" your post if I could. It is referring to gcc 2.8.1 so I guess it is written around 1998.

This topic is closed to new replies.

Advertisement