views:

110

answers:

3

Early today I discovered function try-catch blocks (from here in fact) and then went on a bit of a research spree - apparently they're main use is it catch exceptions throw in by a constructor initialiser list.

Anyway, this sent me thinking about failing constructors and I've got to a stage where I just need a little clarification. This is all just me trying to learn more about the language, so I don't have a practical example, but here goes...


Given this example code:

class A
{
private:
    B b
    C *c;    //classes B, C & D omitted for brevity as not really relevant
    D d;
public
    A(int x, int y, int z)
};

A::A(int x, int y, int z)
try
    : b( x )
    , c( new C(y) )
    , d( z )
{
    //omitted
}
catch(...)
{
    //omitted
}

What happens in these cases:

  1. The initialisation of b throws an exception.
  2. The initialisation of c throws an exception.
  3. The initialisation of d throws an exception.

Specifically, I want to know at least:

  • what will/may cause a memory leak from new C(y). I'm thinking only 3? (see here)
  • could you just delete b in the catch? Is dangerous in cases 1 and 2?

Obviously, I guess the safest thing to do is to make c a smart pointer. But disregarding that option for the moment, what's the best course of action?

Is it safe to set c to NULL in the initialiser, and then place the call to new in the constructor body?

That would then mean a delete c must be placed in the catch in case something else throws in the constructor body? Are there safety issues doing that (ie, if it's the c = new C(y); itself that throws)?

A: 

To answer your actual question, any time an exception occurs during construction, execution stops right there (the object being constructed is never constructed) and control passes to the nearest exception handler (catch).

You're right that a memory leak will occur only when d causes an exception, and in that case, the outer catch needs to clean up.

If new C itself throws, the object is never constructed, so you don't need to worry about deleting it.

casablanca
Any suggestions on how the catch could safely clean up? I'm getting the feeling it cannot, and you get left with either a possible memory leak or the risk of deleting an uninitialised pointer.
DMA57361
In a way, you're right - it would leak to awkward situations and you can't always guarantee a clean solution. If `b`, `c` and `d` all throw different exceptions, you could figure out where exactly the failure occurred and clean up accordingly.
casablanca
+4  A: 

Try/catch function blocks are frowned upon, in the same way as goto --there might be some corner case where they make sense, but they are better avoided: when an object fails to be constructed, best thing you can do is fail and fail fast.

On you specific questions, when an exception is thrown in a constructor, all fully constructed subobjects will be destroyed. That means that in the case of b it will be destroyed, in the case of c, it being a raw pointer, nothing is done. The simplest solution is changing c to be a smart pointer that handles the allocated memory. That way, if d throws, the smart pointer will be destroyed and the object released. This is not related to the try/catch block, but rather to how constructors work.

It is also, in general, unsafe to delete the pointer from the catch block, as there is no guarantee of the actual value of the pointer before the initialization is performed. That is, if b or c throw, it might be the case that c is not 0, but is not a valid pointer either, and deleting it would be undefined behavior. As always there are cases, as if you have a guarantee that neither b nor c will throw, and assuming that a bad_alloc is not something you usually recover from, then it might be safe.

Note that if you need to keep a pointer with a raw pointer for some specific reason, it is better to initialize it to 0 in the initialization list, and then create the object inside the construction block to avoid this problem. Remember also that holding more than one resource directly in a class makes it really hard to ensure that no resource is leaked -- if first resource is created and assigned, and the second one fails, the destructor will not be called and the resource will leak. Again, if you can use a smart pointer you will have one less problem to deal with.

David Rodríguez - dribeas
Yes, it seems the more I think and read about these, the more they really don't seem to help all that much.
DMA57361
Your claim "try/catch blocks are frowned upon, in the same way as goto..." is simply nonsense. try/catch is _the_ language feature to be used to write code with proper error handling in the context of exceptions. How else do you expect exceptions to be handled?
hkaiser
@hkaiser: there has been a misunderstanding here, what is somehow *frowned upon* is function level try/catch blocks: `void f() try { ... } catch () {}`. That feature was added to the language and is fairly unknown, and not recommended. I have clarified the intent --or at least tried-- now. Regular try/catch blocks *inside* functions are, as you correctly state, *the* language feature for error handling. I will try to locate references from well known sources when I get home. This is the first one: [GOTW#66](http://www.gotw.ca/gotw/066.htm)
David Rodríguez - dribeas
DMA57361
@DMA57361: the "best" solution is to use a smart manager for each of the resources: smart pointers for new'ed objects, `std::vector` for dynamically allocated arrays etc... using try/catch to handle the release of resources is akin to using a hammer for performing surgery, it might work, but it's needlessly complicated and not likely to work for that long (maintenance...)
Matthieu M.
@Matthiew M - Oh I agree entirely. I was just trying to get a better understanding of what seems to be (or more accurately *is*) a rather complex area. And, at some point some class is going to have to work around this issue? The smart pointers and the like we're relying on must handle this issue, because they can't use these constructs themselves - but I guess this will mostly be inside the core of libraries, where most of us don't need to really worry about it...
DMA57361
@DMA57361:*...some class is going to work around this issue...* Not really, smart pointers themselves are rather simple and don't really need to handle this. The resource is created externally, if that creation fails then it is automatically cleaned. Then it is passed to the smart pointer, and the smart pointer only copies the pointer --cannot throw. If at a later time anything throws is not really a problem. The class implementing RAII has already been fully constructed can will be destroyed. The destructor can perform cleanup in the simplest possible way.
David Rodríguez - dribeas
+2  A: 

You cannot do anything in the function-try-block handler, except translate one exception to another. You cannot prevent exception from being thrown. You cannot do anything to class members. So no, you cannot do delete c in the constructor's function-try-block.

The fully constructed base classes and members of an object shall be destroyed before entering the handler of a function-try-block of a constructor or destructor for that object.

and

The currently handled exception is rethrown if control reaches the end of a handler of the function-try-block of a constructor or destructor. Otherwise, a function returns when control reaches the end of a handler for the function-try-block (6.6.3). Flowing off the end of a function-try-block is equivalent to a return with no value; this results in undefined behavior in a value-returning function.

(15.3 [except.handle] from the C++ Standard)

atzz
So, basically, the object is destroyed (effectively) as part of an unwind that happens before the `catch` starts. So, in the `catch`, the "current" object doesn't exist; you cannot attempt clean up on an object that's not there.
DMA57361
@DMA57361 - yes. My understanding is that `function-try-block` was added to the language with only purpose of exception translation in constructors, to facilitate joint usage of classes from independent libraries. Also, you can put logging there. That's pretty much it...
atzz