Page 1 of 1

C++ Exceptions

Posted: November 8th, 2019, 9:41 pm
by albinopapa
Perhaps you've heard of them, they are a way of halting a program dead in it's tracks if an irrecoverable error occurs. Say you have a program that depends on a specific file being loaded, but the file is lost or corrupted. There isn't a point in returning an error code since there's nothing you can do but tell the user about such a problem and they must reinstall the program. This is a perfect scenario for throwing an exception.

There are some warnings about exceptions though. One is pretty widely talked about or at least it's something I've heard many people bring up on C++ and exceptions is never let your destructors throw an exception. The reason for this is because when an exception is thrown, the program begins to unwind it's stack. This starts calling destructors for any variables declared locally all the way back to either the beginning of the program or the first catch block it comes across. If during that period of unwinding and calling destructors another exception is thrown the program is required to call terminate(). When this happens, the program quits without calling any more destructors and resources may or may not be released back to the operating system upon program exit.

Now, here's another interesting thing I just ran across. If a constructor throws, it's destructor is NOT called. This makes sense because it means the object is not fully formed so destruction may be meaningless. What happens though to open files or memory that's already been allocated? Well, quite frankly, they are dropped on the floor, gone forever, or until next reboot or maybe the OS will do the cleanup when program ends I'm not sure. I'll have to do some digging I suppose.

That being the case, things become difficult in determining how to handle constructing objects in my opinion. In the case above where your program relies on a file being there and not corrupt, you may want to start putting try/catch blocks around these types and if an exception is thrown, release any other resources that may have been acquired before the attempt at loading the failed resource.

I'll try coming up with examples and get back on some suggestions for handling such behavior. Just something to think about. Unfortunately, this makes delayed initialization more appealing.

Re: C++ Exceptions

Posted: November 9th, 2019, 1:54 am
by cyboryxmen
Having resources leak in constructors because destructors don't run on a failed construction is a common issue that's brought up but it can easily be fixed. You just have to wrap every individual resource in its own ownership object. So instead of

Code: Select all

class X
{
    X()
    {
        arr1 = new int[1000];
        arr2 = new int[1000]; // Could throw and leak arr1
    };
    
    // ...

private:
    int* arr1;
    int* arr2;
};
you do

Code: Select all

class X
{
    X()
    {
        arr1 = std::unique_ptr<int[]>{1000};
        arr2 = std::unique_ptr<int[]>{1000}; // Can throw but arr1 won't leak since it is its own ownership object.
    };

private:
    std::unique_ptr<int[]> arr1;
    std::unique_ptr<int[]> arr2;
};
About not being able to throw in destructors, you can have a resource pool that keeps track of every allocated resource in the program. Constructors get the resources from the pool and destructors simply mark them for deletion. You can then call the resource pool's procedure at a specific point of the application to do mass clean up of all marked resources and fetch the cleanup errors from there. Now you can clean up your application properly and still use RAII!

Exceptions and RAII are just the best. Every time you allocate a resource , you always have to remember to deallocate them in proper order at the end. You can be in a rush to finish the job so you allocate the resource but forgot to clean it up. The program seems to run fine so you don't realise it until much later in the project when you somehow run out of resources. You also have to write an if statement around every single procedure that can throw(A.K.A all of them) to propagate their errors upwards and then have every function in the call chain propagate that upwards too. It's really adds up as your code gets bigger and I don't want to deal with that ever again.

Re: C++ Exceptions

Posted: November 9th, 2019, 2:30 am
by albinopapa

Code: Select all

#include <iostream>
#include <memory>
struct A {
	A( int ) {}
	~A() {
		std::cout << "A::~A() called.\n";
	}
};
struct B {
	B() = default;
	B( int count )
	{
		up_a = std::make_unique<A>( count );
		throw std::runtime_error( "Out of memory" );
	}

	std::unique_ptr<A> up_a;

};

int main( int argc, char* argv[] )
{
	try
	{
		B a = B( std::numeric_limits<int>::max() );
	}
	catch( const std::exception& e )
	{
		std::cout << e.what() << '\n';
	}
	return 1;
}
Cpp.sh online compiler
And just to prove what cyboryxmen has just posted, here's a test. Ok, faith in constructors and exceptions restored, thanks bud.

Re: C++ Exceptions

Posted: November 9th, 2019, 2:41 am
by albinopapa
You can then call the resource pool's procedure at a specific point of the application to do mass clean up of all marked resources and fetch the cleanup errors from there.
So, what you are saying is:
The pool or resource locator handles instantiation of the resources
The objects needing the resources get passed these resources or a reference to the pool/locator and a pointer/reference to the resource is stored within the object
When the object is destroyed, it just sets the pointer to nullptr and somehow flags the resource as being not needed ( reference counted maybe? )
Then when the locator/pool is done and needs to clean up, it calls "release" functions instead of implicitly allowing the resource wrapper to destroy the resource?

Re: C++ Exceptions

Posted: November 9th, 2019, 3:29 am
by cyboryxmen
You only need a counter if it's a shared resource. If the ownership is exclusive like for std::unique_ptr, you just set a flag to mark it for deletion.

Re: C++ Exceptions

Posted: November 10th, 2019, 4:44 am
by chili
Yeah, exceptions and RAII go hand-in-hand. Exceptions are also the only way to signal error directly from CTOR. So if you aren't using exceptions, and you want to signal failure, you have use delayed initialization as you mentioned. But doing so also compromises the integrity of your RAII.

"they are a way of halting a program dead in it's tracks if an irrecoverable error occurs"

I take issue with this sentence being the 1st part of your description of exceptions :lol: That is a really a useful side effect, and not the main purpose of exceptions.