Exceptions

Pages: 123
(Warning: rant)

I think we had a discussion about exceptions before, but I don't remember it going anywhere. http://www.cplusplus.com/forum/beginner/142576/#msg752510
Cubbi wrote:
It's hard to find a guide worse than Google's (the exception ban is only a tip of the iceberg)
LB wrote:
What's so wrong with the exception ban?
Cubbi wrote:
It turns C++ into a fundamentally different language, one without RAII or guaranteed class invariants.
The last time I checked, RAII didn't depend on exceptions, nor were exceptions the only case where RAII was useful. As for invariants, I don't see how removing exceptions removes the guarantee of class invariants. That's like saying that an application which never throws an exception while it runs has no class invariants while it runs. If a tree falls in the forest... it still falls.

I don't like exception handling.

The three common things I've seen happen when you throw an exception are:
- The exception is caught, logged, and ignored (e.g. passing events to plugins)
- The exception is caught and the application handles it (e.g. message box to user, 500 internal server error, etc)
- The exception goes all the way up the entire call stack and ends the application (e.g. ran out of memory)
I need not mention abuse of exceptions to control program flow.

The way you design code with and without exceptions is dramatically different. I've done both in the past. Whenever I've needed to throw an exception it was because I allowed a wider range of inputs than I could actually work with. When I write code without exceptions, I don't allow that. If a conversion needs to take place from an unsafe input to a safe input, that conversion is documented. "Values below 0 will be forced to 0", not "Values below 0 will cause IndexOutOfBoundsException to be thrown".

Let's look at an example.
1
2
3
4
5
6
7
template<typename T>
struct MyList
{
    //...
    T &get(std::size_t index){/**/}
    //...
};
This is a poorly designed interface. What happens when you try to access an index that doesn't exist? You can't take the last element in the list - what if the list is empty? Either you say it results in undefined behavior, or you throw a std::out_of_range exception.

Returning a pointer so that nullptr can be returned is not good design either - that doesn't stop people from neglecting to check if the returned pointer is null and just blindly dereferencing the pointer. Instead, I would prefer this interface:
1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
struct MyList
{
    //...
    T &get(std::size_t index, T &def = default_value()){/**/}
    //...
private:
    static T &default_value()
    {
        static T t;
        return t;
    }
};
Now, in the event that no value is present, you have a default value that can be returned instead and which the user can optionally provide. If the type doesn't have a default constructor then the user just needs to always provide a default value and the code will compile (http://coliru.stacked-crooked.com/a/6d71917f14c7331d). There's no need to inform the user about accessing an out-of-range index because if they cared in the first place they would have called size() first. Telling them about it when they don't care doesn't do anything. Since they work with a default value the state of the program isn't changed.

How will you debug bad code if there's no crash, no stack trace? There's the logs. What would have happened anyway if we threw an exception? It'd probably just get logged and the application would continue running. Same as always.

And think about it: what would the calling code do anyway if it checked the size and found out the index it wanted to reach didn't exist? Is that your concern? Why should it be when the calling code doesn't check? Whether the calling code checks or not doesn't influence whether you check or not.

But if that interface doesn't suit you, try this one:
1
2
3
4
5
6
7
8
9
template<typename T>
struct MyList
{
    //...
    void act_upon(std::size_t index,
                  std::function<void (T &)> if_valid,
                  std::function<void (std::size_t)> if_invalid = [](std::size_t){}){/**/}
    //...
};
It's a bit more of a mouthful, but it now encourages design that is less likely to act weird when given the default value of the previous interface. And it lets the caller decide what to do with an error (again, if they cared they'd have checked the size themselves). The third parameter may as well not be there. Or that third parameter could be passed a throw statement if you disagree with me. In other words, exception handling has just become optional. Amazing.


In C++ there's plenty of places where you can do bad things and the language doesn't throw an exception or guarantee that your program will even continue running, and we've built walls to protect us from those. So why don't we build walls instead of throwing exceptions? It's a great mystery to me.

Or maybe we should add std::null_pointer_exception and freely dereference pointers without ever checking if they're null. That gets checked for us, right? So why do we need to worry? We'll do something about it later when the bug reports start coming in. Hey, the number of times per month you'll need to fix it will go down over the years, don't worry. But those pesky std::invalid_argument_exceptions will still crop up from time to time because you couldn't be bothered to fix bad designs that accept invalid arguments.


Besides everything mentioned above, the number one problem I have with exceptions is that they're basically trolls. They claim to protect you from doing bad things, but they are nowhere to be found at compile time and instead jump out in the last second at runtime and shout "boo!" at whoever happens to be running the program. Essentially they are a workaround for inability to detect certain kinds of problems at compile time.

Plenty of good-quality C code has been written without exception handling. I don't understand why making the language easier and safer to work with would require the introduction of a tool that lets you know you've done something wrong when it's already too late. "Oops, you gave me a value that is outside the range you're supposed to give me! No you're going to have to go back and do what you were too lazy to do before! Boo!"


I apologize for the rant, but exception handling really bothers me. I'm not beyond being convinced that I don't understand exception handling, but I am pretty confident that I do. I can also understand that exceptions can be a necessary evil in certain cases, but that doesn't make them less evil to me. So how many of my points do you disagree with? ;p
Last edited on
The last time I checked, RAII didn't depend on exceptions

RAII absolutely depends on exceptions. Resource Allocation Is Initialization -- it's the whole point.

I don't see how removing exceptions removes the guarantee of class invariants

std::lock_guard has the invariant "the associated mutex is locked". It is true for the entire lifetime of the lock object. How would you achieve that without exceptions? (or, to give a less resourcey example, boost::gregorian::date is always a valid date, regardless of what you pass in its constructor)

Whenever I've needed to throw an exception it was because I allowed a wider range of inputs than I could actually work with

That's called "wide contract", it's a whole different thread.
Last edited on
RAII absolutely depends on exceptions. Resource Allocation Is Initialization -- it's the whole point.
I don't see how RAII depends on exceptions. You spelling out the meaning didn't really help my understanding either.
Last edited on
er, sorry, Acquisition, not Allocation.

I don't see how could anyone think otherwise. If the resource could not be acquired, initialization didn't happen, and that's achieved by throwing an exception (or by terminating the program, I guess that's an alternative).
So, if I am understanding you correctly, the main advantage of exceptions in C++ is that they can be thrown from constructors?
That and they let you write code under the assumption that it will always work, and will jump to a separate error handling portion of code when it fails. Rather than littering your code with dozens of checks for error conditions.
@Disch: I do not allow error conditions to exist, so how could I be checking for them?
Last edited on
Often times it's out of your control.
Yes, I cannot control the C++ standard stream library, which is practically built on error states.
Don't get me started on iostream design decisions. =P (I'm really not a fan)

I mean things like hardware failure, missing files, user error, etc.
Last edited on
Hardware failure: could you be more specific? Ideally the program should stop running to avoid corruption of data.

Missing files: regenerate the default ones

User error: could you be more specific? Ideally the UI will prevent this.
Hardware failure: could you be more specific? Ideally the program should stop running to avoid corruption of data.


Not enough memory for an allocation.
Disk drive is busy/unresponsive.
Network connection was lost.
etc
etc


Missing files: regenerate the default ones


So your answer to keep local copies of all files in the program binary? Why bother with external files at all, then?

Besides it might not be that the file is missing. Maybe it just can't be opened. Like another program has it opened with exclusive access rights.

User error: could you be more specific? Ideally the UI will prevent this


The UI could prevent it with exceptions.
Disch wrote:
Not enough memory for an allocation.
Stop the program.
Disch wrote:
Disk drive is busy/unresponsive.
I was not aware that C++ provided a way to detect this.
Disch wrote:
Network connection was lost.
The libraries I use call a function when that happens.
Disch wrote:
Why bother with external files at all, then?
To let the user modify them? As for assets, load the appropriate inbuilt default asset for that asset type (e.g. pink-and-black checkerboard for missing texture).
Disch wrote:
Besides it might not be that the file is missing. Maybe it just can't be opened. Like another program has it opened with exclusive access rights.
Either I can access the file or I cannot.
Disch wrote:
The UI could prevent it with exceptions.
Who catches the exceptions? The UI is the topmost level - you're now (ab)using exceptions for flow control.
I'm with LB here. Exceptions are borderline shunned in the gaming industry. And I'm really not seeing how not having exceptions means you can't have RAII. Yes, you can throw an exception when you can't establish an invariant, or, you can have code that can't fail to establish an invariant. RAII is more to do with exception safety than it is to do with using exceptions.
@LB:

Yes there are ways around using exceptions.

The point is that they can be useful in those instances. But if you are looking for ways to avoid using them, then of course you're never going to see how they're useful.

you're now (ab)using exceptions for flow control.


Flow control with exceptions is simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
    // code that will always work
    someCode();
    someCode();
    someCode();
    someCode();
}
catch(...)
{
    // what to do when that code didn't work
    handleFailure();
}


The alternative to that is this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if( !someCode() )
    handleFailure();
else if( !someCode() )
    handleFailure();
else if( !someCode() )
    handleFailure();
else if( !someCode() )
    handleFailure();

// or....
bool success = someCode();
if(success)
    success = someCode();
if(success)
    success = someCode();
if(success)
    success = someCode();

if(!success)
    handleFailure();



It's not abusing flow control. It's using a different method of flow control. One where successful code and error handling code are clearly separated instead of being jumbled together.



Lachlan Easton wrote:
Exceptions are borderline shunned in the gaming industry.


Source?

And for what reason?


And I'm really not seeing how not having exceptions means you can't have RAII.


A contrived example:

1
2
3
SomeClassWithNonTrivialCtor foo;

foo.doSomething();


If the ctor fails you have a few options:

1) Throw an exception
2) Have the object in an uninitialized/bad state. IE: foo.doSomething() will crash or do something undefined. (bad bad bad bad)
3) Have the object in an intermediate state that acknowledges it is bad, and prevents further action from being taken (ie, foo.doSomething() will fail, rather than do what it was intended to do)


#3 requires a lot of additional code both in the class (have to check for intermediate state in ALL public members before doing any work)... as well as outside of the class (code has to check to see if construction failed).

#2 is wretched.

#1 is nice and easy. The exception prevents the object from being created and jumps to an error handler.




For a more realistic example:

1
2
3
4
5
{
    std::lock_guard<std::mutex> lock(mymutex);
    // ..code here can assume mymutex is now locked
    doSomethingThatRequiresMyMutexToBeLocked();
}


What if the lock fails? You're screwed. Now instead of an exception being thrown and handled, you get runtime race conditions and weird glitches.
Last edited on
LB wrote:
Besides it might not be that the file is missing. Maybe it just can't be opened. 
Like another program has it opened with exclusive access rights

Either I can access the file or I cannot.
LB wrote:
Missing files: regenerate the default ones
Aaand if files were inaccessible instead of missing attempt to recreate them will fail. What now?

You described what to do, not how to handle.
For example you are loading several resourses from file. You have loaded four, but on fifth file was missing. What to do? Who should handle it? You cannot just hadle it on-place. You should at least notify code which requested reading about regeneration, so it could re-request already loaded resources from new file. If there is another layer of abstraction between reader and res manager , it would have to know not only about things it should handle, but also about things it supposed to pass higher. It looks like hot potato game with error codes. If you change lower level stuff you will have to change code to accomodate new possible errors all way back to code which can handle it. Hello, modularity and independency.

Main advantage of exceptions is that you cannot ignore them. You have to explicitely handle them. Even if unhandled exception would not be thrown now, when it does, it would say explicit cause and ideally would generate crash info you can use to pinpoint the cause. You forget to check error code. Your program works for majority but not for customer far away from you. It is way harder to detect problem in this case.

Every programmer makes mistakes. No one is perfect. Even OS writers and guys from Google make stupid mistakes:
http://www.viva64.com/en/a/0076/
http://www.viva64.com/en/a/0079/
http://www.viva64.com/en/a/0074/
Exceptions provide safer and easier way to detect them (because, as you agreed in previous post, program termination is better than some unexpected behavior). They allows you to produce cleaner code, increase modularity and independence.

Exceptions are borderline shunned in the gaming industry
Last time I checked, managed languages were pretty popular in gaming industry. Now I want to see C# without exceptions.
Seriously, I want to hear one good reason for that aside from false "exceptions are slow" nonsence. Unless your compiler is retarded and uses SJLJ exception model, they are zero-cost.
Yes, you can throw an exception when you can't establish an invariant, or, you can have code that can't fail to establish an invariant
And now we have bunch of outside code which have to know about class invariants and check them before each operation. Good bye encapsulation. And you are not solving problem, you are deferring it to other code. There could still be 15 calls between code where error was detected and code which needs to know about it.
Last edited on
Disch wrote:
Flow control with exceptions is simple: [code that can result in error state] The alternative to that is this: [code that can result in error state]
I don't understand how the alternative is any different.
Disch wrote:
If the ctor fails you have a few options:
Again, why on earth would you allow for the ctor to fail?
Disch wrote:
What if the lock fails? You're screwed.
I have yet to research the new C++11 multithreading classes, but from my understanding, this code blocks until it can continue. I don't understand how it can fail.
MiiNiPaa wrote:
Aaand if files were inaccessible instead of missing attempt to recreate them will fail. What now?
The design I have used in the past will try to load from the file first and if it can't it will fall back on the in-built version. It ignores any failure to resave the inbuilt one. I just used the word failure, you say? Why yes, the C++ standard library did just use the word failure.
MiiNiPaa wrote:
For example you are loading several resourses from file. You have loaded four, but on fifth file was missing.
End of stream is reached, I stop reading the file.
MiiNiPaa wrote:
You forget to check error code.
Where is all this error code nonsense coming from? Did I have a slip of the key and write something about error codes? I don't design code that can enter an error state, so what error codes could there ever be?
MiiNiPaa wrote:
Every programmer makes mistakes. No one is perfect.
I practice the controversial art of protecting myself from me.
MiiNiPaa wrote:
I want to hear one good reason for that aside from false "exceptions are slow" nonsence.
I ignore the efficiency of language features and standard library implementations. I don't care if exceptions are slow or fast.
MiiNiPaa wrote:
And now we have bunch of outside code which have to know about class invariants and check them before each operation. Good bye encapsulation. And you are not solving problem, you are deferring it to other code.
Again you assume that objects may be placed into error states. You're effectively sharing the same disdain for the C++ stream classes as me and Disch.

I would like to know how a std::string can be in an error state. Have you ever checked mystring.is_valid() before using it? I don't remember having to do that.
Last edited on
I would like to know how a std::string can be in an error state
1
2
std::string s = "Hi";
std::string n(std::move(s));
Now Standard requres s to only support safe assigment and deletion. Any other attempts to use it anyway would lead to problems. Implementation can add invariant check inside and for example throw an exception "moved-from object used", or place class into default state (state of empty string for std::string for example) but there are objects without sensible default state. Move semantics usually breaks source object invariants, leaving empty husk behind. By saying "no invalid state may exist" you throw out move effectivness.
Either you will have to add is_valid() member (ugly) or make move operations behave like copy and essentually drop main feature of C++11.
Last edited on
First of all, I would like to once again vindicate the use of exceptions as control flow structures.

I have yet to research the new C++11 multithreading classes, but from my understanding, this code blocks until it can continue. I don't understand how it can fail.
For example, a thread might terminate while holding the mutex. This may or may not be cause for an error when another thread tries to lock the mutex.

I don't design code that can enter an error state
So your code never does any I/O at all?
Last edited on
LB wrote:
I don't understand how the alternative is any different.


It's not. They're both ugly. The point was to show how the version which uses exceptions is simpler.

Again, why on earth would you allow for the ctor to fail?


Again, it might be out of your control.

I'll go back to the "file can't be read" example.

1
2
3
Image foo("someimage.png");

foo.draw(screen);


Say foo can't be loaded because the file doesn't exist and/or can't be read and/or is corrupt.

Your options are:

1) Fall back to loading some default image and use that instead
2) Put the object in an "error state" that has to be polled both in and out of the class
3) Throw an exception.

I'm suggesting approach #3, it sounds like you are suggesting #1.


But #1 is terrible. It's a quiet failure. IE, there's no way to detect that it happened from outside the class.

Also, it does something other than what the user of the class intended. They wanted to load someimage.png... not whatever backup image you had. So now they're going to be [silently] drawing the wrong thing.

What's worse... if this is something more sensitive than an image... like a script or shader or something... it would [silently] cause extremely weird behavior in the program.


Silently faking a success is worse than failing. I would rather my code fail. Then maybe I can do something about it.



I mean... how would you like it if a class like std::string did that? IE, instead of throwing an exception when it can't allocate more space, if it just didn't grow the string? That would be disasterous!
Pages: 123