Exceptions, Construction and Destruction..

Started by
8 comments, last by Helter Skelter 18 years, 9 months ago
Hiya folks, I bring more querilous queries! Or something like that. Anyway, I'm playing with implanting some debugging helpers in my engine early on (doing a major rewrite of 80% of my code), and while that has been all dandy (I've got a call stack trace, full logging, etc), I've been working with keeping track of thrown and caught exceptions, and have hit upon a little problem. Okay, so here's a rough idea of what I'm doing. When an exception is constructed (thrown, generally.. Haven't actually created an exception object and then thrown it later yet [grin]), the line and file are added to a table. Now, when the exception is destructed (which should be after the catch block it was caught in finishes execution), the file and line of that should be added to the table as well. So this seems fairly easy, I'll just add the code to add the data into the constructor and destructor. However, to my dismay, I've discovered that an exception object gets destructed twice... Once when it leaves the try block it was created in, and once when it leaves the catch block it was caught in, without getting constructed again in between. Is there any particular reason why this occurs? Could someone perhaps explain the process of catching exceptions to me, or point out a useful link I could look at? Here's an example of some code that won't work:

int ExceptionTest()
{
	class E
	{
	public:
		E() : ptr(new int(0)) { std::cout << "E Constructor." << std::endl; }
		~E() { delete ptr; std::cout << "E Destructor." << std::endl; }
	protected:
	private:
		int * ptr;
	};

	std::cout << "In ExceptionTest!" << std::endl;
	try
	{
		std::cout << "In the outer Try block." << std::endl;
		try
		{
			std::cout << "In the inner Try block." << std::endl;
			std::cout << "Throwing." << std::endl;
			throw E();
		}
		catch(...)
		{
			std::cout << "In the inner Catch block." << std::endl;
			throw;
		}
	}
	catch(E & x)
	{
		std::cout << "In the outer Catch block." << std::endl;
	}

	std::cout << "Out of the try/catch block." << std::endl;
	return 1;
}

Any help is appreciated! Note that the way the exception is caught has nothing to do with the problem, I tried lots of different combinations.
Free speech for the living, dead men tell no tales,Your laughing finger will never point again...Omerta!Sing for me now!
Advertisement
The exception object is getting copied to a safe location when the exception is thrown. You aren't tracking the copy constructor, so that's probably what it seems like it's getting destroyed twice.
Would catching by reference fix that? :s
Quote:Original post by Zahlman
Would catching by reference fix that? :s


You can still get a temporary 'exception object' created from the object thrown, even when you catch by reference. It is this object that is passed down to subsequent catch clauses after a re-throw.

The standard does allow for this temporary exception-object to be optimized out though, and the original object that was in the throw expression to be used in its place, but only if it does not alter the meaning of the program in a way other than executing the constructor and destructor of the exception-object.
Quote:Original post by Jingo
Quote:Original post by Zahlman
Would catching by reference fix that? :s


You can still get a temporary 'exception object' created from the object thrown, even when you catch by reference. It is this object that is passed down to subsequent catch clauses after a re-throw.


Havn't tried it but including a copy constructor and incrementing a copy count var in the source should be sufficient to detect duplicates in the list of thrown/caught exceptions.

Quote:Original post by SiCrane
The exception object is getting copied to a safe location when the exception is thrown. You aren't tracking the copy constructor, so that's probably what it seems like it's getting destroyed twice.


Ah, that's what I was looking for!

Anyway, thanks for the replies folks, I appreciate the effort [smile]
Free speech for the living, dead men tell no tales,Your laughing finger will never point again...Omerta!Sing for me now!
After thinking about it for a few minutes I came up with an quick example. At first I considered just overloading new/delete to include the filename and line number but there may be problems with that approach (i.e. conflicts with other new/delete implementations such as MFC's DEBUG_NEW macros). The example uses throw_, throw_p, caught_, and caught_p macros to hide the filename and line number requirements and simplify the tracking.

This example is for discussion and experimentation only and should not be used in a production environment. At the very least it should provide an approach that you can adapt and build on to suit your own requirements.

It does not implicitly handle rethrowing of an exception. Optimally you would want to track ALL throw_/catch_ operations rather than just the start and end points of the process. This *CAN* be done with the example by throwing another exception of the same type and supplying the original object to the copy constructor - MESSY but would work.

I used printf because i'm lazy :)


// Need to declare special macros for throwing and handling exceptions// This is mostly just to hide the implementation and requirement for// supplying file and line number. Macros ending in _p throw a pointer// to the exception object (reducing/eliminating temporaries).#define throw_(type, params) throw type params(__FILE__, __LINE__)#define throw_p(type, params) throw (*(new type params))(0, __FILE__, __LINE__)#define caught_(var) var.caught(__FILE__, __LINE__)#define caught_p(var) var->caught(__FILE__, __LINE__)namespace excpetion_test {	class exception {	public:		// Quickie place to store the location information		struct Location {			Location() {				filename = NULL;				lineno = 0;			}			const char *filename;			int lineno;		};	private:		int		count;		const char	*message;		Location	thrown_at;		Location	caught_at;	protected:		// Debug dump		void dump(const char *ptr, Location &src) {			printf("%s: %s (%d)\n", ptr, src.filename, src.lineno);		}		// Copy from source exception		void CopyFrom(exception &src) {			count = 0;			message = src.message;			thrown_at = src.thrown_at;			caught_at = src.caught_at;			src.count++;		}	public:		exception(const char *msg) {			message = msg;			count = 0;		}		exception(exception &src) {			CopyFrom(src);		}		~exception() {			printf("\n\n---------------------------------------\n");			printf("message = %s\n", message);			printf("count = %d\n", count);			dump("thrown", thrown_at);			dump("caught", caught_at);		}		// handle assignment operator		exception &operator=(exception &src) {			CopyFrom(src);			return *this;		}		exception &operator()(const char *filename, int lineno) {			thrown_at.filename = filename;			thrown_at.lineno = lineno;			return *this;		}		// Special case for doing 'throw new exception'		exception *operator()(int ignored, const char *filename, int lineno) {			thrown_at.filename = filename;			thrown_at.lineno = lineno;			return this;		}		// Set the location for where the exception was handled		exception &caught(const char *filename, int lineno) {			caught_at.filename = filename;			caught_at.lineno = lineno;			return *this;		}	};	class exception_test {		static exception_test t;	public:		exception_test() {			test(true);			test(false);		}		void test(bool use_ptr) {			printf("\n\n\n======================================\n");			printf("use_ptr = %d\n", use_ptr);			try {				if(true == use_ptr) {					throw_p(exception, ("throw new exception();"));				}				else {					throw_(exception, ("throw exception();"));				}			}			catch(exception e) {				caught_(e);			}			catch(exception *e) {				caught_p(e);				delete e;			}		}	};	exception_test exception_test::t;}
In the end though, regrettably, this way ins't any more useful then just calling a 'Catch(string file, int line)', to tell you the truth.. As far as I can see, to get the line and file of the catch, you'll always have to input the file and line yourself, you can't get those from a destructor. And the manual calling of the 'Catch function was what I was trying to avoid, because that puts the problem in the end-user's lap [grin] Thanks anyway though, because at any rate, this way I can add functions that should be executed when the exception has been caught, should I ever need to [grin]

BTW, thanks for the example, must have taken a bit of work [smile] Appreciated!
Free speech for the living, dead men tell no tales,Your laughing finger will never point again...Omerta!Sing for me now!
Quote:Original post by Zahlman
Would catching by reference fix that? :s

Not really. When an exception is thrown, the program goes into meltdown mode; it's like a giant vat of acid gets poured on the stack, destroying all objects. So in order for your exception object to make it safely to the exception handler, the exception object can't live on the stack. Generally the program accomplishes this by storing the exception object in a safe place, usually in a writable data segment of the application image. In debug mode this typically works in two stages: the program creates the exception object on the stack and then copies it to the safe area. In release mode on some compilers it may be constructed directly in the safe area.

Quote:Original post by Zahlman
Would catching by reference fix that? :s



To expand on SiCrane's excellent explanation consider the following code:
void func() {	if(false == dosomething()) {		throw false_exception();	}	else {		throw true_exception();	}	throw new heap_exception();}void test() {	try {		func();	}	catch(false_exception e) {	}	catch(true_exception &e) {	}	catch(heap_exception *e) {	}}



Since implementations and optimizations concerning exceptions may vary between compilers I'm going to base this on MS Visual Studio.NET 2003 just so we have a specific point of reference.

When false_exception is thrown the initial copy is created on the stack. At the time it is thrown its constructor is called. As the cleanup begins the local copy (on the stack) is then copied to a safe location in memory. This follows all standard C++ copy conventions and if a copy constructor is available it will be used. When catch(false_exception e) is reached the temporary copy of the exception is copied into 'e' - which is on the stack. Again standard C++ copy conventions are used. By the time it's done 3 copies of false_exception have been initialized and destroyed.

In the case of a true_exception being thrown the only difference is the LAST copy operation which copied the temporary object into 'e'. Since a reference is used '&e' actually points to the temporary copy of true_exception maintained by the runtime exception handler.

Last but not least we have the 'heap_exception' being thrown. Because the exception is allocated on the heap it is unprobable (but NOT impossible) that any additional copies will be made. There are many arguments for and against doing a 'throw new' and it's usually a topic of religion so I'm not going to take it any further than that.


There used to be a readily available test suite that would determine the exact behavior of the runtime exception handlers as well as how the compilers optimized and ordered certain operations. If I can dig it up I'll post a link as it's very helpful for cross platform development.

This topic is closed to new replies.

Advertisement