C++ Book

  • June 2020
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View C++ Book as PDF for free.

More details

  • Words: 5,609
  • Pages: 16
"icoansi ALIGNleft> Exception handling explained in this section is a new feature introduced by ANSI-C++ standard. If you use a C++ compiler that is not adapted to this standard it is possible that you cannot use this feature. During the development of a program, there may be some cases where we do not have the certainty that a piece of the code is going to work right, either because it accesses resources that do not exist or because it gets out of an expected range, etc... These types of anomalous situations are included in what we consider exceptions and C++ has recently incorporated three new operators to help us handle these situations: try, throw and catch. Their form of use is the following: try { // code to be tried throw exception; } catch (type exception) { // code to be executed in case of exception }

And its operation: - The code within the try block is executed normally. In case that an exception takes place, this code must use the throw keyword and a parameter to throw an exception. The type of the parameter details the exception and can be of any valid type. - If an exception has taken place, that is to say, if it has executed a throw instruction within the try block, the catch block is executed receiving as parameter the exception passed by throw. For example: // exceptions include
In this example, if within the n loop, n gets to be more than 9 an exception is thrown, since myarray[n] would in that case point to a nontrustworthy memory address. When throw is executed, the try block finalizes right away and every object created within the try block is destroyed. After that, the control is passed to the corresponding catch block (that is only executed in these cases). Finally the program continues right after the catch block, in this case: return 0;. The syntax used by throw is similar to that of return: Only the parameter does not need to be enclosed between parenthesis. The catch block must go right after the try block without including any code line between them. The parameter that catch accepts can be of

any valid type. Even more, catch can be overloaded so that it can accept different types as parameters. In that case the catch block executed is the one that matches the type of the exception sent (the parameter of throw): // exceptions: multiple catch blocks include Exception: index 10 is out of range
In this case there is a possibility that at least two different exceptions could happen: 1. That the required block of 10 characters cannot be assigned (something rare, but possible): in this case an exception is thrown that will be caught by catch (char * str). 2. That the maximum index for mystring is exceeded: in this case the exception thrown will be caught by catch (int i), since the parameter is an integer number. We can also define a catch block that captures all the exceptions independently of the type used in the call to throw. For that we have to write three points instead of the parameter type and name accepted by catch: try { // code here } catch (...) { cout << "Exception occurred; } It is also possible to nest try-catch blocks within more external try blocks. In these cases, we have the possibility that an internal catch block forwards the exception received to the external level, for that the expression throw; with no arguments is used. For example: try { try { // code here } catch (int n) { throw; } } catch (...) { cout << "Exception occurred; }

Exceptions not caught If an exception is not caught by any catch statement because there is no catch statement with a matching type, the special function terminate will be called. This function is generally defined so that it terminates the current process immediately showing an "Abnormal terminationerror message. Its format is: void terminate(); Standard exceptions

Some functions of the standard C++ language library send exceptions that can be captured if we include them within a try block. These exceptions are sent with a class derived from std::exception as type. This class (std::exception) is defined in the C++ standard header file <exception and serves as a pattern for the standard hierarchy of exceptions: exception

"16linea2 =16 =16 ALIGNleft>bad_alloc "16linea2 =16 =16 ALIGNleft>bad_cast "16linea2 =16 =16 ALIGNleft>bad_exception

(thrown by new) (thrown by dynamic_cast when fails with a referenced type) (thrown when an exception doesn't match any catch) (thrown by typeid)

"16linea2 =16 =16 ALIGNleft>bad_typeid "16linea2 =16 =16 ALIGNleft>logic_error "16linea1 =16 =16 ALIGNleft>"16linea2 =16 =16 ALIGNleft>domain_error "16linea1 =16 =16 ALIGNleft>"16linea2 =16 =16 ALIGNleft>invalid_argument "16linea1 =16 =16 ALIGNleft>"16linea2 =16 =16 ALIGNleft>length_error "16linea1 =16 =16 ALIGNleft>"16linea3 =16 =16 ALIGNleft>out_of_range "16linea2 =16 =16 ALIGNleft>runtime_error "16linea1 =16 =16 ALIGNleft>"16linea2 =16 =16 ALIGNleft>overflow_error "16linea1 =16 =16 ALIGNleft>"16linea2 =16 =16 ALIGNleft>range_error "16linea1 =16 =16 ALIGNleft>"16linea3 =16 =16 ALIGNleft>underflow_error (thrown by ios::clear) "16linea3 =16 =16 ALIGNleft>ios_base::failure Because this is a class hierarchy, if you include a catch block to capture any of the exceptions of this hierarchy using the argument by reference (i.e. adding an ampersand & after the type) you will also capture all the derived ones (rules of inheritance in C++).

The following example catches an exception of type bad_typeid (derived from exception) that is generated when requesting information about the type pointed by a null pointer: // standard exceptions include
You can use the classes of standard hierarchy of exceptions to throw your exceptions or derive new classes from them.

Understanding C++ Exception Handling by Steve Crocker

Introduction Exceptions have been around in C++ for a while and are pretty common. If you use the STL or even just new you have been exposed to exceptions. Of course, like all things in C++, having a language feature does not necessarily clearly point one in the direction of the best use of that feature. I would not dare to call myself an expert or authority on exception handling, but as a user of C++ I've had some exposure to using exceptions. This article will offer some insight into the use and potential misuse of exceptions. I highly recommend reading Scott Meyer's More Effective C++; there is a whole section devoted to exceptions. Bjarne Stroustrup's The C++ Programming Language Third Edition also has some excellent information on exceptions. I am assuming that the reader is familiar with the basics of exception handling and hopefully has read over Meyer's items on exceptions. I also use a few terms from John Lakos's Large Scale C++ Software Design. This book is invaluable for understanding how a logical design should be translated into a collection of namespaces, header files, source files and libraries. I mostly refer to components and packages.

Overview of C++ Exception Handling C++ Exception Handling is centered around the three keywords: try, catch, and throw, the general purpose of which is to attempt to execute code and handle unexpected exceptional conditions, hence the name. This consists of utilizing a try block (with its attendant handlers). Here's a simple code snippet that should look familiar: try { if ( ! resourceAvail ) throw MyExceptionClass( "Resource Is Not Available" ); } catch(MyExceptionClass& myException) { // resource was not available, do something cleanup throw; }

The code checks to see if a resource is available and if not, throws an exception. The handler for the MyExceptionClass presumably does something meaningful about the exceptional state. From this oversimplified example we can see a typical use of exceptions which is to prevent continued operation if the program cannot obtain the required resource. Another example of this use of exceptions is new(), which will throw the standard exception bad_alloc if the required amount of memory is not available. (Unless, of course, you've changed the new handler.)

Implications of Using Exceptions To support exceptions and stack-unwinding, the process of calling destructors on leaving a scope, compilers put in some initialization code to ensure that if a routine may return via an exception it will properly call destructors. This seems to imply that stack-unwinding is a big part of the fixed cost regardless of whether or not you use a try/catch block. Even if you only use primitive types, if you make calls to functions, which is highly likely, then you probably end up paying this cost. So it is probably best to assume that this fixed cost is a part of the cost of a function call, and potentially, scope change if that scope change has local variables. This means there is additional code being executed. This can consist of function calls that the compiler automatically inserts into the code. Some compilers may allow this code to be generated inline to improve performance, such as C++ Builder. So if we are probably going to be paying this cost, then why worry about how many try/catch blocks there are? Because it is best to partition error handling code from regular execution code. This keeps the intent of the code clear. Another important fact about exceptions is that they affect program flow control. This is very significant particularly during the debugging process. A thrown exception can transfer control to a catch in a very distant location as well as, of course all the way up to main(). This can wreak havoc with figuring out the origin of the exception. Just consider this innocuous source listing: try

{ SomeFunc(); AnotherFunc( 16 ); YetAnotherFunc( EvenMoreFunctions( 2 ) ); }

catch(...) { // catch all exceptions and cleanup throw; }

Now try debugging this when an exception is generated by a function that EvenMoreFunctions() calls. And it only happens some of the time. Granted, if you're in a debugger you can just enable the 'Break on C++ Exception' option and viola. However, if this a bug report from a tester, or worse, customer, and it rarely happens, well then you have some trouble. It is not just the throw of an exception which impacts program flow control. The catch clause or more specifically, clauses can have a significant effect as well. When the exception is thrown it will be transferred to the first matching catch block of the exception type thrown. This can be conceptualized as a special kind of switch statement; and a switch statement affects flow control.

Exception Specifications When I first started learning about exception specifications I flipflopped quite a bit about how useful they are. I could see the benefits from a client perspective, but the implications imposed on the component writer made me reconsider where exactly they should be applied. The essential benefit of an exception specification is that clients of a component know exactly what exceptions may be thrown from a function or a method. This can be great from the client perspective of handling exceptions, because the client knows exactly what exceptions, if any, may be thrown. However, as a component implementer, the cost of guaranteeing meeting that expectation may be high. For example, let's consider the following code. class MyClass { void MyMethodWithOnlyOneExceptionSpecification(void) throw (MyExceptionClass); void MyMethodThatThrowsNoExceptions(void) throw(); };

As a client of MyClass you can say, "Great! I only have to worry about MyExceptionClass exceptions coming out of MyMethodWithOnlyOneExceptionSpecification() and no exceptions will be thrown from MyMethodWithThrowsNoExceptions()." In actuality, the first exception specification says that exceptions of type MyExceptionClass or derived from MyExceptionClass may be thrown from that method. But maybe that isn't such a big deal after all. As a client you may only care about the generic MyExceptionClass. However, to implement this specification we will most likely have to use a try/catch block within the implementation of these methods to enforce the exception specification. Otherwise we run the risk of std::unexpected() being called, the default behavior of which is to terminate the application. Clients of our class may override this default behavior, but as a component implementer we cannot make this assumption. And since aborting the program execution is usually much more catastrophic than letting an exception propagate all the way up we have to fall back on using the try block. This means you could be imposing performance penalty for your exception specification, above and beyond the performance penalty of the exception specification, that is. Granted, maybe you could conditionally compile the try block in debug versions of your component and or package, but that seems rather dangerous. Because of this problem, it seems best to avoid exception specifications for most components. So where does it make sense to use exception specifications? My experience has indicated that if you need to enforce an exception specification, either of a specific exception class or no exceptions, then you should probably do so at the highest level components of your package. And do so only when you know you must. Another potential location for using exception specifications is in thread routines as throwing an exception out of a thread routine is analogous to throwing one out of main(). Real-time systems, such as games, may benefit from not permitting exceptions to be thrown from certain portions of their API. This is because of the effect on flow control exceptions have. In this case it may be beneficial to use exception specifications to enforce this.

Designing With Exceptions When implementing the packages which make up your application, such as your core graphics engine, it is very useful to define an exception hierarchy to utilize. This is useful for delineating kinds of

exceptions as well as translating error codes into useful string messages. An example of this is the standard exceptions of the Standard C++ Library. A simple base class exception type can contain an error message string into which you can place your exception information. Specifying constructors which take parameters that specify the file and line number ( which typically can obtained from __FILE__ and __LINE__ ) and a string message allows for an easy way of storing the origin of the exception. You can also put a simple method for logging the exception which can be called in the constructor of a concrete exception class. I've also defined exception classes which translate DirectX error codes into a human readable message. If a component in the game engine package throws an exception it is always of a type derived from the base exception class of the package. In general, if you are going to throw an exception, you might as well gather up as much knowledge about the exception then because you are already paying a huge performance penalty for throwing the exception. Also, if the act of throwing your exception happens to cause memory exhaustion, there is guaranteed to be enough memory free to throw a bad_alloc exception. My experience has been that you'll likely get terminated by the operating system before bad_alloc is thrown, however in either case you're already in a world of hurt. I'll admit that I've opted-out of using exception specifications in my DirectX wrapper package in order to avoid adding superfluous try blocks. Also, since the exception classes themselves log the error message I can make my catch blocks either catch-all handlers or for my base exception class and still log exception information automatically. It is probably best to use the specific handler in places where only your own exception classes will be thrown and use the catch-all when making calls into APIs which may be throwing anything; this second case includes calling new() and its potential throw of bad_alloc. And I generally have one catch handler for the try block in order to simplify program flow control.

"Resource allocation is initialization" or Stack-Based Resource Management This technique, as described Margaret A. Ellis and Bjarne Stroustrup in The Annotated C++ Reference Manual, by Stroustrup in The C++

Programming Language Third Edition and further elaborated on by Meyers in More Effective C++ is well documented. I usually prefer to think of it as Stack-Based Resource Management because it tells me a little more about what is going on and what the goal is. A great example of this is the Standard C++ Library's auto_ptr() template class, which is specifically designed for this purpose. Here's an example of how it is typically used: void Func(void) { auto_ptr myClass( new MyClass( ... parameters ... ) ); myClass->Something(); }

There is a number of cool things about this simple example. First, we don't have to worry about deleting the heap object in the case of an exception or normal execution; the auto_ptr's destructor handles this for us. This also means we have one path of execution regardless of whether or not any exceptions occur; either memory exhaustion or those thrown by the constructor of the class. This solution also scales well to having many heap allocated local objects. Even better, we do not have a try block! So we've already simplified the code significantly. By using a different kind of smart pointer ( auto_ptr does not necessarily have the best copy semantics for this ) we can also apply this technique to classes which have multiple heap objects as members. This way we can properly handle construction without memory leaks and without a try block. Granted, maybe allocating multiple heap objects is going to cost more in terms of time than the try block initialization is, so maybe its a bit unnecessary from a performance standpoint, but it does make the constructor less cluttered. Another common use of this technique is for synchronization mechanisms such as critical sections. If a portion of code or an entire routine needs to be synchronized, a simple object can be used to enter the critical section in the constructor and leave the critical section on destruction. I've seen this referred to as a guard or lock. The fundamental is what Meyer's succinctly describes in More Effective C++ Item 9 - use destructors to prevent resource leaks. I usually tend to think of it as putting code which must execute, regardless of how a block is exited, into the destructor of an object on the stack. In auto_ptr

the resource is memory, in a critical section the resource is the lock on the critical section. No doubt you can think of other examples which may be more involved than these simple examples. This is really one of the most useful aspects of exception handling: stack unwinding. And not only does it make the code easier to read and understand it also can be used to eliminate try blocks, which can help improving application performance.

Interactions with Threads There are a few important things to keep in mind when dealing with exceptions in a multi-threaded program. Most are pretty straightforward but if forgotten can cause a number of problems. First is that each thread routine is akin to main() and you should not allow exceptions to be thrown out of the thread routine. Provided you created the thread using _beginthread(), and properly initialized the runtime library, your application will exit indicating abnormal program termination. Otherwise you will likely cause the operating system to display an unsightly message about your application and offer to terminate it for you. This is actually worse than throwing an exception from main() which should just indicates abnormal program termination. Trying to use an exception specification on the thread routine in order to get it to call your installed unexpected handler does not seem to work. The application just terminates executes as if no exception specification existed. I'm not sure if this is considered a bug in my compiler or not. The bottom line is that each thread routine should be created using _beginthread() and include a try-block and a catch-all handler just like main() generally does. Next is that each thread routine has its own stack, and hence exception chain. This means that the current exception has to be per thread data and re-throwing an exception is, obviously, only valid within a single thread. The implication is if you have multiple threads which may be working with the same component or group of components and one causes an object to enter an invalid state due to an exceptional condition, your other threads will probably not be aware of this, even if you are properly synchronizing access. The problem is that an exceptional condition in one thread has no way of indicating to another thread that such a condition has occurred. And when your other thread is scheduled it will probably end up

encountering an exception as well. This means that an exception in one thread should probably stop the other related threads as well. Of course, subtle problems can arise in how those other threads are stopped. I've usually used an approach where one thread, maybe the main application thread, is ultimately responsible for stopping the dependent threads if one of them stops running. It may also be responsible for stopping other threads and itself if one of the threads it is dependent upon stops running. I've also generally taken the approach that exceptions should get propagated up to the thread routine and should cause the thread to exit, in a normal fashion, which would then allow the managing thread to notice one thread it is dependent upon is not running and shut down all relevant threads.

Exceptions in Real-Time Systems Real-Time systems may be a bit of an overloaded term, but it is applicable to games. After all, what is a computer game other than a software-based real-time system? Otherwise, why would we complain about low frame rates? My experience, however limited, with game programming and other real-time systems have led me to my current belief that real-time systems should not throw exceptions out to their clients. By this I mean a high level component, perhaps the main package interface, which may have a number of sub-components which make up the realtime system. These sub-components, particularly if well insulated from clients can throw exceptions to their heart's content, but the main engine control interface probably should not. Under what would be exceptional conditions it should shut down and indicate such an error condition to the client. It is fine if the client of that engine then used that error code to throw an exception based on it, but that decision has been pushed into application level code. This makes the engine more flexible for clients. One problem with throwing exceptions is that there may be other realtime systems which are running concurrently and interdependently. This may cause one of them to not stop properly. Not to mention the issues involved with stopping a real-time system cleanly, particularly a multi-threaded one.

Exception Handling Philosophies The fundamental reason for throwing an exception is to stop program execution along the current path. This generally means an error condition has occurred and normal program flow cannot continue. The

best benefit to this is that client code can be simplified and yet still allows the implementer of a component to guarantee that if an error occurs the client will be aware of it. Basically, it removes the responsibility of a client to check error codes. There seem to be three common reasons for throwing an exception: 1. A programming error - a pointer is null when it should not be 2. A resource is not available for acquisition or release - bad_alloc on memory exhaustion 3. A violation of a class invariant or a state of an object invariant - such as an invalid parameter is passed to a method or a calculation that goes out of range

So the reasons to throw an exception seem pretty straightforward. The more difficult question is where to handle exceptions. This is can be a lot more complicated an issue because of the flow control aspect of exceptions. We have to look at what most try/catch blocks are trying to accomplish. Generally, I've seen try/catch blocks trying to do a few different things: 1. Prevent resource leaks 2. Stop an operation after it has failed 3. Stop an operation deciding how to continue based on the cause of the error

As we've seen from "Resource allocation is initialization" the first use of a try/catch block is completely unnecessary. We can eliminate the need for the try block, improving the clarity of the code, by using auto_ptr and similar classes to handle the releasing of resources in their destructors. This can also work for thread synchronization objects ( at least ones which you are not going to timeout on, such as a critical section ) and can be applied to other forms of resource acquisition. So, we can easily, relatively speaking, remove the try/catch blocks which are only preventing resource leaks and improve program performance and clarity. The second use of a try/catch block is probably the most coherent use of exception handling. At some level in the system we attempt to perform a, usually, complex task which may fail in a number of ways. In this use of a try/catch block we are only concerned with complete success. No matter what the failure is we handle it in the exact same manner. Informing the user of the cause of the error can still be handled in a generic way, particularly if you have your own exception hierarchy. Your base class of your exceptions can provide a method for translating an exception into a human readable message that can be

presented to the user. This allows the user to know why the operation failed and yet keeps the flow control simple in the error handler case. The last example of exception handling seems at first like a simple permutation on the second one, but there are number of consequences involved. Having more than one catch block amounts is a form of branching and introduces more complex flow control. There are instances where this is unavoidable but hopefully you can minimize them. Maybe you handle only a few specific exceptions and the others are not handled. Perhaps these are from an unrelated exception hierarchy or you must handle specific exception classes in unique ways. The critical question to consider is whether or not the system is handling the exception at the given location. Handling the exception can usually be read as not re-throwing it. If you are re-throwing an exception then you're not really handling it, you're just trying to return your object or system to a valid state. And if you find yourself having to do this in a number of different ways depending upon the exception thrown then you are potentially creating future maintenance problems. The reason for this is that it implies that proper error handling follows a chain of exception handlers that are spread across different levels of the system. To minimize this complexity you should let the exceptions go up to the highest level possible in the system, usually the point at which the application, or highest level components of the package, initiated the operation. Similarly, a thrown exception should always propagate outside of the function which generated the exception. They should also usually propagate out of the component which generated them. This follows the idea that exceptions are a non local form of error handling. If you throw an exception in within a function and handle it directly in that function then it is being used as a form of flow control and can obscure that flow.

Potential Pitfalls Having run into a number of pitfalls in using exception handling, I'd like to offer some things to watch out for. Mainly these focus around understanding the system at run time and conceptualizing program flow control.

Knowing when to re-throw an exception and when not to can sometimes be difficult. It is far better to err on the side of caution and always re-throw the exception unless you are a thread routine or highlevel component controlling a real-time system. This is at odds with deciding whether or not you are handling an exception and are not rethrowing it. The issue really hinges on the organization of your components and the level in the system at which they exist. Generally this means the top level components in the package or application should have most of the exception handlers. The reason being is that if you do not re-throw the exception you are potentially introducing complex flow control that is not apparent when inspecting the code. Although using the technique of logging each exception on its construction can help alleviate this somewhat. This tends to indicate that you should minimize exception handlers in lowlevel components, using them only for freeing resources. When used this way, they can be replaced with objects on the stack which perform this resource management automatically. Also, if you find yourself writing duplicate cleanup code in an exception handler and the regular function body, particularly if this is a catch(...) handler, it can made clearer using the such techniques as applying auto_ptr. Whenever this duplicate code exists it indicates such code is intended to execute under any condition of exit of the current scope; such as a destructor call for local objects on the stack. A more thorny issue has to do with using multiple catch handlers for different types of exceptions. It is important to remember that each catch handler is a separate branch of execution. If you find yourself doing different things based upon the type of exception you catch you are walking down a potentially dangerous path. Basically, this amounts to two bad things, one is case analysis of object type, generally considered bad and using exceptions as a form of messaging. The problem with case analysis is similar to other problems with case analysis. If a new exception object is introduced into the system it may need to have its own handler added and address the new 'case' of exception. Even worse is that if the catch handlers do radically different things then the application's behavior becomes dependent upon what exception it catches. This leads us into the second problem. Since the program behavior is being guided by the type of exception caught a particular location you can get unexpected behavior caused by a different origin point of the exception. If the same exception is thrown from a different location in code that is called within the try block with the switch statement like catch handlers, then the program

flow control will transfer to that handler for a different reason. This new reason may be just different enough from the origin set of assumptions that it causes subtle bugs. This can be particularly troublesome if this occurs in a low-level component that may not be accessible to the client. Basically unless you write and maintain all the code that your try block executes you cannot safely make assumptions about the origin of an exception. Even if you do, the components are likely to be in different locations and their flow control interactions will not be immediately apparent. This can create a kind of pseudo-event handler mechanism and the problem, in this context, with the event handler model is you do not know the origin of the event, only the occurrence of it. It is far safer to assume that you have no idea what specific exceptions may be thrown from where, unless exception specifications are present, but there's that whole can of worms. Unfortunately, oftentimes this form of exception type analysis is exactly the only viable solution. The thing to try to keep in mind is to avoid this as much as possible. And it can be mitigated a bit by trying to have those specific exception handlers at the highest level of organization possible. This is also true of re-throwing exceptions as well; let them bubble up as far as possible.

Conclusion Exception handling is obviously a powerful feature of the C++ language. Although you can walk down the path of not using exceptions at al, their benefits far outweigh the costs. The key is to understand how exceptions operate; how they interact in a game application or other real-time system; where to use them and not use them; and to understand their effect on performance. Exceptions can greatly simplify the development of a C++ package and provide the component writer a way to enforce the assumptions made when the component was implemented. I would be glad to hear of other's experiences with exception handling. Just send me an email at [email protected] - Steve Crocker

Virtual destructor Implement all of your destructors as virtual if your class serves as a base class and will be derived from. In other words, if there is a remote chance that other classes ever derive

from it make the destructor virtual. This way proper destruction of derived objects will occur.

Related Documents

C++ Book
June 2020 8
[romanian Book]c++ -cuprins
December 2019 6
The C Puzzle Book
April 2020 7
C%2b%2b Book
November 2019 34
C- Book 01
November 2019 1
[romanian Book]c++ - Bibl
December 2019 9