Print Story Programming
Software
By codemonkey uk (Wed Jul 21, 2004 at 11:36:00 AM EST) (all tags)
I was going to post a diary about my German lesson today, but I got into an interesting discussion over a couple of beers about programming, and specifically object life time management in C++.

More specifically, explicit Initialise / Destroy member functions, vs just using C++ construction / destruction semantics.

So I'm going to talk about that instead.



First of all, I should say that my current positions is strongly on the side of using the C++ language constructor/destructor semantics, and that putting creation and destruction in member functions that need to be explicitly called is one of my pet hates.

Second of all I should say that the people I was discussing this with are Very Smart people who I have a lot of respect for. That said, assuming I remember the discussion correctly, two were strongly in favour of explicit creation and destruction in member functions, one was mildly in favour of them, and one was mildly in favour of using C++ construction / destruction semantics, so I was in the minority.

Finally, there was no clear consensus, nor was there any specific sub-issue that the different sides of the issue strongly disagreed with. In the end the argument just petered out without anyone changing their minds.

Feel free to correct me, or comment on what follows. I am half-cut, so I make no claims for technical or journalistic accuracy here, but I want try and resolve this in my own mind. Either I am wrong, and I should change my stance, or I am right, and I need a stronger argument to present when I encounter this issue again, or both approaches have their advantages or disadvantages, and depending on the context of the design decision either could be right or wrong. In which case, I should try to identify which context is the right place to apply which approach.

So, here are the arguments, as I remember them:

General

Context: Games programming.

Explicit initialisation and destruction prevents accidental hidden construction/destruction that can be highly detrimental to performance. Implicit construction - creating an object on the stack - can look like an inexpensive operation, even when it is not. Requiring programmers to use explicit initialisation/destruction forces them to think about these issues up front, and saves time fixing performance issues later on.

Good interface design, using explicit constructors, taking care when to pass by reference or pointer and when to pass by value, avoiding casting, being careful about operator overloading is an equally effective way to avoid unwanted copies and object conversions. Requiring explicit destruction can cause bugs such as resource leaks which could be harder to track down than the overhead from implicit destruction.

My conclusion: This argument really feels like a premature optimisation to me, and the worry about resource leaks, combined with the extra work of dealing with an interface that requires objects to be explicitly initialised seems to outweigh any gains that can be had in avoiding premature pessimisation through accidental object creation/destruction inside performance critical loops. At the end of the day, a profiler can find a performance hole much quicker than a programmer can find a bug, and if your object construction/destruction is performance critical, either move it out the loop, or optimise it.

Exceptions

Context: Where the use of exceptions is inappropriate (I agree that such contexts exist).

In this context implementing constructors that fail is problematic, because of the lack of a return value. Using explicit initialisation one can implement a constructor that returns a boolean on failure, and check for it.

Constructors that can fail do so because of a failure to acquire a resource. In such cases, a class should be used that acts as a smart pointer to that resource. You can attempt to acquire the resource via a function that returns a smart pointer to it, check the result and on success pass it to the constructor of the object.

My conclusion: In my experience classes that can fail on construction are the exception, not the norm, and by requiring the acquired resource to be created by the calling code one makes the resource requirements, and thus the potential cause of failure of the class explicit. The passed in resource smart-pointer can be checked In a debug build via an assertion, providing some sort of assurance that a partially constructed object will not be used.

Memory Management

Context: Delegating memory management to the client of a library.

Okay, I honestly don't get this one - if a Initialise function can take a memory block as an argument, then why can't you pass that same memory block to the constructor. Or use placement new. Or have a custom allocator of some sort. I think I've forgotten an important detail of the pro-explicit argument here, so I think I'll call it a day for this diary, and hope to get some interesting feedback...

End

Finally, I am left wondering if we were really talking about the same thing: Were they really arguing for:

Object object;
object.Init(arg);
//...
object.Destroy();

Or were they trying to sell me a factory pattern without knowing the name?

PS: Also, pro-constructor, why pay for default initialisation, followed by creation of member variables when an initialiser list will do the job in one hit?

< Interesting dilemma | BBC White season: 'Rivers of Blood' >
Programming | 8 comments (8 topical, 0 hidden) | Trackback
Yes by ucblockhead (5.00 / 1) #1 Wed Jul 21, 2004 at 11:59:19 AM EST
I often add this to headers:

private:
  Foo();
  Foo(Foo&);
//etc

Then don't actually define them. Stops worries about "hidden" construction.

The exception argument is (IMHO) the only good one as throwing exceptions is problematic. In such cases, I prefer something more like this:

Foo f;
if( f.isValid() ) {
  //use f
}

and in the object itself:

void Foo::checkValid()
{
  if( !m_Valid ) throw Exception("Invalid");
}

void Foo::myFunc()
{
  checkValid();
  //code
}

The memory management is somewhat moot if you avoid new, though obviously you can't always do that. Not only do clients then get to decide the storage method, but if you use the heap, the allocated chunks are bigger. I'm not strong on this argument because I admit that I've never used C++ in a context where memory management was problematic.
---
[ucblockhead is] useless and subhuman


But by codemonkey uk (3.00 / 0) #5 Wed Jul 21, 2004 at 07:21:13 PM EST
I dislike all the extra code for checking the validity of the object that you end up with. To me it seems to outweigh any minor advantages that you might gain in working around not using exceptions. Again, my argument for this construction argument is to acquire the scarce resource separately, and pass it into the constructor, thus avoiding in the general case the need for an isValid member variable, function, and calling code. Writing less code means getting more work done.

Note here that we are talking about general recommendations for the development of an application of library, not how to handle one of special cases.

--- Thad ---
Growing a mustache for charity.
[ Parent ]

My take by Evil Cloaked User (5.00 / 1) #2 Wed Jul 21, 2004 at 01:56:07 PM EST
Use an constructor that sets a member variable to true if it all works and then whenever you create an object, immediately afterwards, call a function that checks the value of that var.


--
Still, I think most of the problem is just a mental hurdle to overcome, - Cloaked User


Why? by codemonkey uk (3.00 / 0) #4 Wed Jul 21, 2004 at 07:08:49 PM EST
Doing that means you have code littered with "isValid" flags and tests. Why do you think this is better than having a system that guarantees that constructed objects are valid? What advantage does it bring you?

--- Thad ---
Growing a mustache for charity.
[ Parent ]

on your side by 606 (5.00 / 1) #3 Wed Jul 21, 2004 at 05:56:37 PM EST
I'm on your side: ctors and dtors are the way to go, and explicit inits are the devil. However, I am primarily a Java coder, so some of the arguments seem trivial to me.

The number one thing against implicit initialization is that people always forget to call it.

a = new Foo();
a.doStuff()
Exception: a not initted

Whenever I see this I ask WTF, if the thing has to be inited before it is useful, put the init in the ctor. If the object needs access to other objects first, put them in the ctor. One major error I see is people using uninited objects.

I can see no places where throwing an exception from a ctor would be bad practice. One of the most common errors I see is when people create an object and forget to check if it's been inited (isValid()), and then go on and use it and cause holy hell. It's especially bad because some coders forget to put the isValid() check in the functions they write.

Creating a Factory method that returns null on failure is no better, since people will invariably forget to check for the null case. The program will chug along happily and then fifteen minutes later it will access the null object and die with a NullPointerException. The Exception from the ctor forces people to deal with the fact that initialization may fail. I've seen no other surefire ways of enforcing this.


-----
imagine dancing banana here


Thought by djotto (5.00 / 1) #6 Wed Jul 21, 2004 at 09:23:28 PM EST
Are the pro-explicit guys old enough to have cut their teeth on C?

I'm just wondering if they're of the "I'd never trust the compiler to do this stuff" school of thought, in which case you were caught up in a religious war, so no wonder nobody's mind got changed.

(Seems dumb to have language constructs specifically designed to do X, then subvert them, but I'm still lousy at thinking in OO, so what would I know.)



Yes by codemonkey uk (3.00 / 0) #7 Wed Jul 21, 2004 at 09:41:26 PM EST
Both the strongly-pro explicit people are either currently working at, or were previously working at the same company that sold a large multi-platform software library that (very) recently switched from C to C++.

But then, these are Smart Guys, and people I do not expect to use misconceptions about what the compiler does and does not do as an argument in of it self.

Not knowing what the compiler is supposed to do be fixed by checking the standard. Ignorance can be cured, and opinions changed based on new information. My points about idioms for preventing "hidden" construction/copys were understood, but did not change opinions.

That said, being a vendor that switched from C to C++ means that you may have customers who are hard line old school C who would need to be appeased. In such a case the explicit construction idiom could make good business sense, but then, the context of the discussion was technical, not commercial.

In the same tone, working with a team that is not skilled in C++, and knowing there is no time to train that team could be a good argument for not using C++, or for only using a subset of it.

But something about that argument bothers me. In such a context people should be professional enough to not use features of a language the do not full understand, and should be proactive enough to find out more. A team lead should make it their responsibility to make sure that the team understand the tools it is using, rather than saying "don't press this button or the world will explode".

--- Thad ---
Growing a mustache for charity.
[ Parent ]

Quoth the Rogerborg by Rogerborg (5.00 / 1) #8 Wed Jul 21, 2004 at 09:57:28 PM EST
My insight into C/C++ and resources in general is that if you don't catch a leak within minutes or hours of creating it, you're pretty much not going to catch it.  Being explicit doesn't help.

My specific thought about C++ and games performance is that all optimisation is premature, and that you should build in timing instrumentation from the very start.  In practice, it's far more likely that you'll benefit from culling and doing frequent things infrequently than in making frequent things more efficient.  YMMV, and I bow to your superior and current wisdom in this.

My meta insight is that managed languages make the whole problem moot, and the increased development speed more than makes up for the lower performance and increased acute memory overheads.  Every hour not spent tracking down a memory leak can be spent on investigating and improving performance.  C# fscking r0xx0rz.

-
Metus amatores matrum compescit, non clementia.


Programming | 8 comments (8 topical, 0 hidden) | Trackback