Important Announcement
PubHTML5 Scheduled Server Maintenance on (GMT) Sunday, June 26th, 2:00 am - 8:00 am.
PubHTML5 site will be inoperative during the times indicated!

Home Explore All of Programming [ PART I ]

All of Programming [ PART I ]

Published by Willington Island, 2021-09-02 03:28:09

Description: All of Programming provides a platform for instructors to design courses which properly place their focus on the core fundamentals of programming, or to let a motivated student learn these skills independently. A student who masters the material in this book will not just be a competent C programmer, but also a competent programmer. We teach students how to solve programming problems with a 7-step approach centered on thinking about how to develop an algorithm. We also teach students to deeply understand how the code works by teaching students how to execute the code by hand.

Search

Read the Text Version

variety of poses and animations). The game also has enemies (a variety of monsters, villains, or whatever, which are controlled by the computer), which also have hit points, magic points, a position on the screen, and a collection of images. Unlike the hero, the enemies do not gain levels, but instead have a method to strategize— which implements the game’s decision-making process for that particular enemy. The game also has “power ups” (items that help out the hero or an enemy if they “pick it up”), as well as projectiles, which are for fireballs, bullets, or whatever our hero and villains attack each other with. The power ups and projectiles also each has a point for its position on screen, and a collection of images. The hero, enemies, power-ups, and projectiles all have a method to draw themselves on the screen, and each has a way to test for collisions with the other types. Instead of writing a description of what the classes have, it is quite useful to draw out their relationships (especially as they become more complex). Figure 18.3 shows a diagram of a naïve design of the class relationships for this hypothetical game. In this diagram, each box represents a class and has three parts. In the top part of the box is the name of the class. The middle part contains the fields of the class, and the bottom part contains the methods of the class. We draw an arrow from one class to another to illustrate a “has-a” relationship. The has-a relationship would be implemented with a field in the class at the source of the arrow. The arrows are labeled with the name of the field (on the top) and how many the class has on the bottom. For example, the hero has one point for her position. However, she has one or more (indicated by 1..*) images. The bottom portion of the box lists the methods of the class. In designing a real game, these classes may be somewhat more complex, but this simplification works for the purpose of our example.

Drawing these class relationships is not only a useful tool for novice programmers but an important piece of what professional software engineers do in designing large systems. In fact, what we have drawn here is basically a UML class diagram though with some details omitted.3 Software engineers use UML diagrams not only to plan their class hierarchies but to describe them in an unambiguous way to the other members of their software development team. UML is an example of the old saying “a picture is worth a thousand words.” We will not cover UML in any significant detail, but you should be aware it exists. If you plan on a professional career in software development, you will likely take a software engineering class and learn much more about it. Figure 18.4: An improved version of our class hierarchy, using inheritance. Looking at Figure 18.3, we can see that many of these classes have much in common. All of them have a Point for their position and a collection of Images, as well as methods to draw themselves, check for collisions, and update their positions. From examining these similarities, we should realize the opportunity for inheritance. All of these classes not only have these same fields and behaviors, but they all have them because they are different specific types of the same more general type

—”things that we draw in our game.” Of course, ThingThatWeDrawInTheGame is not a great name for a class, so we should pick a better one for the parent class we are considering. Fortunately, there is a technical term for this concept, sprite—a 2D image that is drawn onto the screen but is not the background image. With this insight, we might revise our inheritance hierarchy as shown in Figure 18.4. This new diagram introduces the Sprite class as the parent class of the Hero, Enemy, Projectile, and PowerUp classes. Note that the arrow with the unfilled triangular arrow head indicates the subclass relationship. All of the common functionality is pulled up into the Sprite class, avoiding duplication of code. Notice how now that all of these types are subclasses of the Sprite class, we no longer need 11 different checkCollision methods. Instead, we can just write one checkCollision method in Sprite, which checks for a collision with some other Sprite. Polymorphism lets us pass in any subtype of Sprite we need to. Even though our second iteration of this design is a significant improvement over the first, we still have some duplication of code. If we look at the Hero and Enemy classes, we will see that both of them have hitPoints and magicPoints. Additionally, the PowerUp class has two different methods, one to apply its benefits to the Hero and one to apply its benefits to an Enemy. Presumably, these two methods have significant duplication in what they do—for example, if the PowerUp is a healing potion, they both add some value to the Hero’s or Enemy’s hitPoints. Observing this duplication, we should then contemplate whether there is some other class C we should write, such that a Hero is a C and an Enemy is a C. Is there a C, such that it has hitPoints and magicPoints, it

makes sense to apply a PowerUp to a C, and a C is a Sprite? We consider these criteria because we want to place the class in the class hierarchy as the parent class of Hero and Enemy (thus the first two constraints), we want to bring the duplicated functionality into this class (thus the second two constraints), and the class would need to be a child class of Sprite (thus the last constraint). In this particular case, creating a Creature class makes perfect sense, as it satisfies all of these criteria. Figure 18.5: An even better inheritance hierarchy. Based on this analysis, we would then proceed to revise our class hierarchy as shown in Figure 18.5. Here, we have introduced the abstract Creature class (in UML, an italicized name indicates that the class or method is abstract) as the parent class of Hero and Enemy. Now, our PowerUp class has a single method allowing it to be applied to a Creature. While this example has been a great illustration of how to find opportunities to use inheritance, we would like to note that this example hierarchy is by no means complete. Beyond the fact that many of these classes would likely have other methods and fields, we would

probably have many more classes. We would likely want a variety of subclasses of Enemy, PowerUp, and Projectile to implement different behaviors for different types of each of those. In fact, these three classes would likely be abstract, as each actual Enemy would be some subtype of Enemy—you would not have something that is “just an enemy.” With these classes made abstract, some of their methods would also be abstract. Hero could have subclasses too, if your game has a variety of heroes with different behaviors. Additionally, while this design could certainly be implemented (and is excellent for demonstrating inheritance concepts), it does not adhere to some other software engineering principles that we might like in a real system. Most notably, it combines the user interface (everything related to drawing and user interaction) together with the model (the data and state of the game world). In engineering programs that interact with end users, the MVC (Model, View, Controller) paradigm is quite popular. This paradigm splits the design into three pieces: the model (which holds all the data and state), the view (which handles drawing that state), and the controller (which handles receiving input and updating the model accordingly). Each of these pieces would have many classes, but no class would be in both parts. Instead, the view queries the model for information and draws accordingly, and the controller calls methods on the model to update its state. A variant of this paradigm that combines the view and controller (which can be somewhat hard to separate sometimes) is called UI Delegate. We are not going to cover them in detail, but you will learn about them if you take a software engineering class. 18.9 Practice Exercises

Selected questions have links to answers in the back of the book. • Question 18.1 : What is inhertiance? When is it appropriate? What is the benefit? • Question 18.2 : What is subtype polymorphism? • Question 18.3 : Given the following classes: 1 class A { 2 int x; 3 public: 4 void something() { ... } 5 }; 6 7 class B : public A { 8 int y; 9 public: 10 void anotherFunction() { ... } 11 }; If you try to write this code: 1 A * ptr = new B(); 2 ptr->anotherFunction(); You will receive a compiler error. Why? • Question 18.4 : In the previous code, we declared class B : public A. What does public mean here? • Question 18.5 : What does protected mean? • Question 18.6 : What does it mean to override a method? How is overriding different from overloading a method? • Question 18.7 : What is the output when the following C++ code is run? 1 #include <iostream> 2 #include <cstdlib> 3 4 class A { 5 protected: 6 int x;

7 public: 8 A(): x(0) { std::cout <<\"A()\\n 9 A(int _x): x(_x) { std::cout <<\" 10 virtual ~A() { std::cout <<\"~A()\\ 11 int myNum() const { return x; } 12 virtual void setNum(int n) { x = 13 }; 14 15 class B : public A { 16 protected: 17 int y; 18 public: 19 B(): y(0) { std::cout <<\"B()\\n\"; 20 B(int _x, int _y): A(_x), y(_y) { 21 std::cout <<\"B(\"<<x<<\",\"<<y<<\") 22 } 23 virtual ~B() { std::cout <<\"~B() 24 virtual int myNum() const { retur 25 virtual void setNum(int n) { y = 26 }; 27 28 int main(void) { 29 B * b1 = new B(); 30 B * b2 = new B(3, 8); 31 A * a1 = b1; 32 A * a2 = b2; 33 b1->setNum(99); 34 a1->setNum(42); 35 std::cout << \"a1->myNum() = \" << 36 std::cout << \"a2->myNum() = \" << 37 std::cout << \"b1->myNum() = \" << 38 std::cout << \"b2->myNum() = \" << 39 delete b1; 40 delete a2; 41 return EXIT_SUCCESS; 42 } • Question 18.8 : If a child class needs to pass parameters to its parent class’s constructor, how would you specify what parameter values to pass? • Question 18.9 : Why is it important for polymorphic classes to have virtual destructors? • Question 18.10 : If a class has a virtual method, can it be a POD type?

• Question 18.11 : What is an abstract method (also called a “pure virtual function”)? How do you declare one? What is it useful for? 17 Templates19 Error Handling and Exceptions Generated on Thu Jun 27 15:08:37 2019 by L T XML

II C++18 InheritanceIII Data Structures and Algorithms

Chapter 19 Error Handling and Exceptions In the real world, things do not always go as expected. Accordingly, programs deployed in the real world must check for and handle situations when problems occur. A problematic situation for a program might be improperly formatted input from the user (e.g., the program wants the user to input an integer, but the user types xyz), inability to open a file, failure of a network connection, or a wide variety of other issues. Similarly, the problem may come from a variety of sources: the user of the program, a logical error in the program itself, a problem with the computer’s hardware, an unplugged network cable, or any number of other issues. Whatever the type and source of the problem are, the program must deal with the situation in some fashion. The worst possible way to deal with any problem is for the program to produce the wrong answer (or action, or lack of action) without informing the user of the problem —a silent failure. Consider the case in which the software to control an airplane’s flight systems cannot lower the landing gear properly. Failing to do so without notifying the user (i.e. pilot) of the situation is a disastrous course of action. A course of action that is slightly better in most cases is to abort the program (or at least the operation) and inform the user of the problem. Producing no answer at all is preferable to producing an incorrect answer—

imagine software to target a missile: we would prefer the missile to self-destruct rather than hit the wrong target. When the program deals with an error by aborting, it should do so with the explicit intention of the programmer (i.e., the programmer should call assert or check a condition then call abort), and the program should give an error message to the user. Simply segfaulting due to an invalid memory access is never appropriate. Of course, in some situations, aborting the program may be an unacceptable course of action—in our landing gear example, the flight control software’s abrupt termination would make the problem worse and therefore be a terrible decision. We would prefer to handle the error more gracefully. Graceful error handling requires making the user aware of the problem (unless the program can fix it), as well as allowing the program to continue to function normally in all other regards. Ideally, we would present the user with some options to remedy the situation (an opportunity to enter a different input, select a different file, retry a failed operation, or engage in some course of remedial/corrective action). Whatever the course of action, the programmer should have explicitly thought of and accounted for the potentially problematic conditions in the code’s design and written her code accordingly. As your programming skills develop, you should get into the practice of writing bullet proof code—code that can gracefully handle any problem that can happen. You should get in the habit of contemplating every possible failure mode as you write your code and determining how you can handle it. When you write a line of code, you should ask “what can go wrong with that line?” and come up with plans for those cases.

In thinking through how to handle a problematic condition, the first question a programmer should consider is whether or not the erroneous situation can be handled directly in the function she is writing or if the function should notify its caller of the failure and allow the caller to handle the problem. This decision should be guided by considering whether or not the function in question can properly handle the error in all cases. If it can handle the error properly, it should. If it cannot, it should inform its caller and allow the caller to handle the error (or inform its caller, and so on). How a function propagates an error to its caller in C++ will be the subject of the rest of this chapter. In C, functions return a value indicating the error, which the caller must check. However, C++ provides a better approach (called exceptions). Before we discuss how exceptions work, let us examine the C-style approach and understand why we would prefer a better option. 19.1 C-Style Error Handling In C, error handling involves checking the return value of a function that you call and possibly returning an error to your function’s caller as needed. For example, suppose we were to write some code that reads input from a file. It might look something like this: 1 someStruct * readInput(const char * filename) { 2 FILE * f = fopen(filename, \"r\"); 3 if (f == NULL) { 4 return NULL; 5} 6 someStruct * ans = malloc(sizeof(*ans)); 7 if (ans == NULL) { 8 fclose(f); //maybe check this too 9 return NULL; 10 } 11 while (...) {

12 ... 13 someType * ptr = malloc(sizeof(*ptr)); 14 if (ptr == NULL) { 15 //code to free up all memory we have allocated so f 16 fclose(f); //maybe check this too 17 return NULL; 18 } 19 ... 20 } 21 fclose(f);//maybe check this too 22 return ans; 23 } 24 void someOtherFunction() { 25 ... 26 someStruct * input = readInput(inputFileName); 27 if (input == NULL) { 28 //do whatever we need to do here 29 } 30 ... 31 } Here, we have elided most of the body of the code, and just written a few select parts the merit some error checking—file opening and memory allocation. We generally want to check fclose but might get away with not doing so in this case, as we are only reading from the file. Looking at this code for a bit highlights several problems with C-style error handling. First and foremost, it is easy to forget (or be lazy about). If we fail to check the return value of any particular function, we might never notice the problem during testing. In the case of fopen, we will discover the error during testing if we test our program with the name of a file that cannot be opened, which should be high on the list of things we try during the testing process. However, testing cases such as the failure of malloc or fclose are a bit more difficult. Forgetting error checks may seem minor; however, they may result in silent failures, crashes, or even security vulnerabilities.

This problem is exacerbated by the fact that there are a wide variety of functions we are accustomed to always succeeding, and we do not really even think about the possibility of failure. For example, printf can fail, but most programmers do not even know this fact much less think about it when they write a call to printf (it returns the number of characters printed or a negative value on an error). Does the fact that printf can fail mean you always need to check its return value? That really depends on the consequences if the call to printf fails. However, you should always think about the possibility of failure. A second problem with this style of error handling is that it “clutters up” the code. Even though rigorous error handling is critical to writing bulletproof (and thus correct) code, it can feel cumbersome to write as well as “in the way” to read through. This downside is particularly problematic, as it makes it easier for lazy programmers to feel justified in omitting error checks that really should be present. A third problem with the error handling in the above example is that we only know that an error has occurred and have no additional information about what went wrong. In this example, when someOtherFunction checks the return value of readInput, it can only test if the return value is NULL (indicating an error) or not. In the case of an error, someOtherFunction cannot determine why readInput failed—the input file could not be opened, a memory allocation failed, or some other reason. Not knowing the reason for the error makes it more difficult to take corrective action. The typical approach to this third problem is to set errno (recall from Section 11.1.1 that errno is a global integer) to an error code indicating what went wrong. The

header file errno.h not only defines errno but also the various standard error codes, such as ENOMEM (insufficient memory), ENOENT (the file was not found), and about a hundred others. Most C library functions will set errno appropriately when they fail, so if the failure stems from the library call immediately before, we do not need to set errno ourselves. However, as errno is global, we must be careful of other library calls that might change its value if we plan to use it. There are a handful of functions that work with errno, such as perror (which prints a description of the error the current value of errno represents) and strerror (which returns a char * pointing at such a string). Of course, we generally want to avoid global variables, so such an approach is nonideal. However, if we are programming in C, it is what we have to work with. 19.2 C++-style: Exceptions From the previous discussion, we can observe three key characteristics we desire in an error handling mechanism. First, we want to remove the possibility that an error can be silently ignored. If a particular piece of code does not handle an error, the error should propagate outwards to that function’s caller. This process should continue until a piece of code capable of explicitly handling the error is found. Second, we would like error handling to be as unobtrusive as possible in our code—reducing the “cluttered” feeling given by C-style error handling. If a function is going to allow an error to propagate to its caller, we should generally be able to write no extra code for that to happen. However, our error handling mechanism should work in such a way that the error propagation does not cause us to leak resources, nor

leave objects in invalid states. Of course, just because we do not have to write anything does not mean that we do not have to think about what happens. The third aspect we desire in our error handling mechanism is the ability to convey extra information about the error. In particular, we would like to be able to convey arbitrary1 information about the problem. Beyond just informing the caller of the type of problem, we might want to more specific and include detailed information about the problem. To achieve all of these goals, C++ introduces exceptions, which involve two concepts: throw and try/catch. A programmer places code where an error might occur (that she has a plan to handle) in a try block. The try block is immediately followed by one or more catch blocks. Each catch block specifies how to handle a particular type of exception. Code that detects an error it cannot handle throws an exception to indicate the problem. This exception can be any C++ type, including an object type. The function that has detected the problem can include any additional information it wants to communicate to the error handling code by including it in the object it throws. Once an exception is thrown, it propagates up the call stack, forcing each function to return (such that each of their stack frames is destroyed) until a suitable exception handler is found, i.e. until it is caught with a try/catch block. Each time a stack frame is destroyed, destructors are invoked as usual to clean up objects; however, if one of these destructors throws an exception that propagates out of the destructor, the program crashes. Therefore you should generally design your destructors to ensure this does not happen.

This style of error handling conforms to the design goals we laid out at the start of this section. Code that can handle an error describes how to deal with that error with try/catch blocks. Code that cannot handle an error can simply do nothing, and the error will propagate to the caller—it cannot be ignored. As the exception is an object, it can inform the exception handler of the type of problem (via what type of object it is) and can encapsulate other information about the situation as needed. 19.3 Executing Code with Exceptions Having learned the fundamental concepts of exception handling, we now turn our attention to understanding in more detail what happens when code throws an exception. Throwing an exception is accomplished by executing a throw statement,2 which is the keyword throw, followed by an expression that evaluates to the exception to throw. For example: 1 throw std::exception(); This expression constructs an unnamed temporary (of type std::exception via its default constructor) then throws the resulting object. The exception then propagates up the call stack, forcing functions to return and destroying their frames, until it finds a suitable exception handler in the form of a try block with a following catch block that can catch an exception of a compatible type. In C++, we can throw any type of object, but should generally only throw subtypes of std::exception. To use std::exception, you should #include <exception>. There are also a variety of built-in subtypes of std::exception, which typically require you to #include <stdexcept>. You

can read about them in the documentation for std::exception (http://www.cplusplus.com/reference/exception/excepti on/. Of course, you can also create your own subtypes of std::exception by declaring a class that inherits (publicly) from std::exception or any of its existing subtypes. Once an exception is thrown, control is transferred to the nearest suitable exception handler, possibly unwinding the stack (forcing functions to return, destroying their frames, and executing the destructors for objects in those frames). Exception handlers are written with try and catch. When code might throw an exception and the program knows how to handle the situation, the programmer writes the code within a try block. She then writes one or more handlers, each of which specifies a type of exception to catch. For example: 1 try { 2 //code that might throw 3} 4 catch(std::exception & e) { 5 //code to handle a generic exception 6} If an exception is thrown within the try block, including the functions it calls (unless they catch it), control transfers into the exception handler (catch block), to allow the program to deal with the situation. More specifically, when an exception is thrown, the following steps occur: 1. The exception object is potentially copied out of the frame into some location that will persist through handling. The “potentially” qualifier appears here as the compiler may eliminate the copy as long at it does not change the behavior of the program (other than removing the extra copy

constructor and destruction). The compiler may, for example, arrange for the unnamed temporary to be directly allocated into some other part of memory (which it will handle). 2. If the execution arrow is currently inside of a try block, then it jumps to the close curly brace of the try block (if any variables go out of scope, they are destructed appropriately) and begins trying to match the exception type against the handler types in the order they appear (if it is not inside a try block, go to step 3). If the execution arrow encounters a catch block capable of catching the exception type that was thrown, the exception is considered caught, and the process continues in step 4. If no handler is found, step 2 repeats (note that the execution arrow is now outside of the try block where it started—it may be inside another if they are nested—or may not be, in which case, step 2 will direct you to go to step 3). 3. If the execution arrow was not currently inside of a try block in step 2, then the exception propagates out of the function it is in. Propagating the exception out of the function destroys the function’s frame, including any objects inside it. In the process of destroying the frame, the destructors for the objects are invoked as if the function had returned normally. The execution arrow then returns to wherever the function was called (again, much like the function had returned), and step 2 repeats. 4. Once an exception is caught, it is handled by the code within the catch block. The first step of handling the exception is to bind (a reference to) it to the variable name declared in the () of the catch block. Then the code in the catch block is executed according to the normal rules, with one exception. This one exception is that if a statement

of the form throw; (throw with no expression following it—just a semicolon after it ) is encountered inside of the catch block, then the exception being handled is re-thrown (the exception handling process starts again from step 2—no extra copy is made). 5. If/when the execution arrow reaches the close curly brace of the handler, then the program is finished handling the exception. The exception object is deallocated (including invoking its destructor), and execution continues normally at the statement immediately following the close curly brace. Video 19.1: Executing code with exceptions. Video 19.1 demonstrates the behavior of a program when an exception is thrown and caught. There are slight variations on the try and catch blocks that are worth mentioning. For the catch block, one can specify that it will catch any type, but in doing so, it cannot bind the exception to a variable. This generic catch is accomplished by placing three dots (...) in the parentheses where one would normally write the type of exception to catch and the name to bind it to. That is, we can write: 1 try { 2 //code that might throw 3} 4 catch(...) { 5 //code to handle any exception 6} Since we are catching an exception of an unknown type, we cannot bind it to any variable, as there is nothing we can do with that variable.

For a try block, there is a variation called a function try block, which is primarily of use in a constructor, where the programmer wants to catch exceptions that may occur in the initializer list. In a function try block, the keyword try appears before the function body (and before the initializer list, if any) and the handlers appear after the close curly brace of the function’s body. For example, to use a function try block on a constructor, we might write: 1 class X { 2 Y aY; 3 Z aZ; 4 public: 5 X() try : aY(42), aZ(0) { 6 //constructor’s body here as normal 7} 8 catch(std::exception & e) { 9 //code to handle the exception 10 } 11 //other members here as normal. 12 }; If an exception occurs in the initializer list or the body of the constructor, it will be caught by the handler after the function (assuming it matches). However, a function try block on a constructor has a special behavior: since the object cannot be properly initialized, the exception is automatically re-thrown (as if throw; were the last line of the function try block. Anything that was successfully constructed (including the parent-class portion of the object) is destroyed before entering the handler. For a “normal” function, a function try block covers the entire body of the function and may return as normal. In fact, if the function returns a value, the function try block should return a value (else you will be warned for “control reaches end of non-void function”).

19.4 Exceptions as Part of a Function’s Interface Functions may include an exception specification—a list of the types of exception it may throw. Such a declaration is added to a function by writing throw() with the exception types listed in the parentheses. An empty list of types in the parentheses specifies that the function may not throw any type of exception, whereas completely omitting the exception specification means that the function may throw any type of exception. For example: 1 int f(int x) throw(std::bad_alloc, std::invalid_a 2 int g(int * p, int n) throw(); 3 int h(int z); The first of the functions in the example, f, is declared to throw two possible exception types, std::bad_alloc and std::invalid_argument. The second function is declared to throw no exceptions. The third does not include an exception specification, so it may throw any type of exception. The exception specification is part of the interface of the function. It tells the code that uses the function what types of error conditions might result from the use of that function. Code that uses functions with exception specifications knows to either handle those errors (if appropriate) or declare them in its own exception specification (as the un-handled exception would propagate out of the caller to its own caller). When overriding a method that provides an exception specification, the overriding method (in the child class) must have an exception specification that is

the same or more restrictive than the exception specification of the inherited method. That is, if f above were a method in a parent class, a child class could override it with any of the following four exception specifications: 1 //Option 1: same as the parent class 2 int f(int x) throw(std::bad_alloc, std::invalid_a 3 //Option 2: more restrictive: cannot throw std::invalid_ar 4 int f(int x) throw(std::bad_alloc); 5 //Option 3: more restrictive: cannot throw std::bad_alloc 6 int f(int x) throw(std::invalid_argument); 7 //Option 4: more restrictive: cannot throw any exception 8 int f(int x) throw(); The reason for this restriction arises from polymorphism. If the child class’s method can throw exceptions the interface of the parent class’s method did not allow, then the child class is no longer a suitable substitute for the parent class. That is, we may have a pointer that is statically declared to be a pointer to the parent class but actually points at an instance of the child class. The code where this pointer exists may then invoke the overridden method, expecting the exception behavior promised by the interface in the parent class. However, if the child class’s method throws unexpected types, the calling code is unequipped to handle those properly. From a software engineering standpoint, providing a correct exception specification for every function you write makes for better code. In doing so, you are more clearly specifying the interface of the function, providing better information to anyone (including yourself in the future) who uses the function. As we will see in Section 19.7, thinking carefully about exception behavior is a crucial part of writing professional-quality code. Including a correct exception specification for a function is a key

part of this process in two ways. First, you should think through the exception behavior of your code fully enough that writing a correct exception specification is easy. Second, reasoning about the exception behavior of your code requires knowledge of the exception specifications of the functions it calls—if they do not clearly specify this behavior, you must delve into their implementation to find details that should be provided in the interface. Unfortunately, C++’s exception specifications were defined in a way that makes them orders of magnitude less useful than they could and should be. Not only do some experts advise against writing such specifications, but also C++11 has deprecated3 them. The reason for this gap between the potential and actual usefulness arises from the fact that the exception specification is not checked by the compiler. Instead, the compiler must generate code that enforces the guarantees at runtime (imposing an overhead). As we will momentarily discuss, violating the exception specification (by throwing an unexpected type of exception) is generally handled in a very heavyweight fashion: the program is killed. 19.5 Exception Corner Cases If you have fully grokked the preceding information on exceptions, you should have a variety of questions about corner cases: “What if an exception propagates outside of main?” “What if a destructor throws an exception during stack unwinding?” “What if a function throws an exception that is not allowed by its exception specification?” “What if my code executes throw; (with no operands) while no exception is being handled?”4 C++ has two special functions, unexpected and terminate to handle such situations.

The first of these, unexpected(), is called when a function throws an exception that is not allowed by its exception specification. The default behavior of unexpected() depends on whether the exception specification allows std::bad_exception. If so, then the unexpected() function throws a std::bad_exception and exception handling continues normally. If std::bad_exception is not permitted, then unexpected() calls terminate(). As its name suggests, the default behavior of the terminate() function is to terminate the program by calling abort(). This function is called in the other situations described above (an exception that propagates outside of main, an exception that propagates out of a destructor during stack unwinding, throw; when no exception is being handled), as well as by the default implementation of unexpected(). We have referenced the default behavior of these functions; however, the programmer may supply her own behavior if desired by calling set_unexpected or set_terminate, passing in a function pointer to specify the new behavior of calls to unexpected() or terminate() respectively. The user-supplied functions for either of these may not return. In the case of a terminate handler, the supplied function must terminate the program. In the case of an unexpected handler, the function may either throw an appropriate exception or terminate the program. Another set of corner cases arise when an exception is thrown during object construction. When such a situation occurs, the pieces of the object that are already initialized are destroyed, and the memory for the object is freed (even if it was allocated with new). In the case of an array of objects (e.g., allocated with new[]), if the object’s constructor throws an exception, then the objects

from indices down to will be destructed in reverse order of their construction. 19.6 Using Exceptions Properly When used properly, exceptions simplify the task of writing code that is capable of recovering gracefully from error conditions. However, when used improperly, exceptions can make the code significantly worse. We provide the following guidelines to help you understand how and when to use exceptions. Exceptions are for error conditions. A program should throw an exception to indicate that an error condition has occurred that which must be handled by the caller (or its caller, or its caller, etc.). If the function can handle the situation properly itself, it should do so. Likewise, you should not use exceptions for the normal execution path of a piece of code. Throw an unnamed temporary, created locally. T he only way you should throw an exception is throw exn_type(args);. Note that there is no new in that statement (you should not do throw new exn_type(args)). Re-throw an exception only with throw; If your exception handler must re-throw an exception, then you should do throw; (with no arguments). Even though you have the exception object bound to a name (e.g., if you wrote catch(exn_type_name & e) { ... }, you should not do throw e; to re-throw the exception. Doing so makes an extra copy of the exception object.

Catch by (possibly constant) reference. You should always catch exceptions by reference, or by const reference. That is, your handlers should look like catch(exn_type_name & e) or catch(const exn_type_name & e). They should not look like catch(exn_type_name e) (catching by value) or catch(exn_type_name * e) (catching by pointer). Declare handlers from most to least specific. If you declare multiple handlers (catch blocks) for the same try block, you should declare them in order from most specific type to least specific type. The handlers are matched in their order of appearance, and if the thrown exception type is polymorphically compatible with the type the handler declares, then that handler is used. If a handler specifying a child class appears after a handler specifying a parent class, the child class handler will never be used, as the exceptions of that type will be caught by the handler for the parent class. That is, if child_exn_type inherits from parent_exn_type, then the following code is poorly written: 1 try { 2 //code 3} 4 catch (parent_exn_type & e) { 5 //handler code 6} 7 catch (child_exn_type & e) { //bad, wil 8 //handler code 9} Destructors should never throw exceptions. Nev er. If they perform any operation that could

throw, you must find a way to handle it appropriately (with try/catch). Exception types should inherit from std::exception. If you write your own exception class, it should inherit (publicly) from std::exception or one of its subclasses. Keep exception types simple. If you write your own exception class, it should be quite simple. Most specifically, it should not have any behavior that can throw an exception. Typically, you will want the exception class to have no dynamic allocation at all (new can throw std::bad_alloc). Override the what method in your own exception types. The std::exception class declares the method: 1 virtual const char * what() const thro This method provides a description of the exception that happened. Note that it returns a C-style string (just a const char *, not a std::string), as those are simpler, and constructing a std::string may throw an exception. Often, you will override this method to just return a string literal. Be aware of the exception behavior of all code you work with. Whenever you call another function, you should be aware of what exceptions it might throw. Knowing this aspect of the code’s behavior is crucial to understanding your own

code’s exception behavior, as well as to writing exception safe code, which is the topic of the next section. 19.7 Exception Safety One of the aspects that distinguishes amateur code that generally works from well-written C++ code suitable to a professional programmer is exception safety—what guarantees the code makes in exceptional circumstances. We alluded to this briefly earlier in Section 15.3.2 when we presented two different versions of an assignment operator: Strong Exception Guarantee 1 IntArray & operator=(const IntArray & rhs) { 2 if (this != &rhs) { 3 int * newData = new int[rhs.size]; 4 for (int i = 0; i < rhs.size; i++) { 5 newData[i] = rhs.data[i]; 6} 7 delete[] data; 8 data = newData; 9 size = rhs.size; 10 } 11 return *this; 12 } No Exception Guarantees 1 IntArray & operator=(const IntArray & rhs) { 2 if (this != &rhs) { 3 delete[] data; 4 data = new int[rhs.size]; 5 for (int i = 0; i < rhs.size; i++) { 6 data[i] = rhs.data[i]; 7} 8 9 size = rhs.size; 10 } 11 return *this;

12 } The second piece of code makes no exception guarantees. The new operator may throw an exception (if the memory allocation request cannot be satisfied). If this exception is thrown, it will propagate outside of the operator= (which is not equipped to handle this circumstance). However, by the time the exception is thrown, data has been deleted, and the state of this object is invalid. Even if the code that has called the assignment operator can handle a memory allocation failure exception, the object it attempted to assign to is in an corrupted state (its data pointer is dangling) and will cause the program to crash when it is used or destroyed (the destructor will double free the data). While such code would get by fine in an introductory programming course, it is not really correct—in the real world, problems happen, and the program must be able to deal with them. Perhaps more importantly, when other parts of the program try to deal with the problem, their job becomes impossible when code makes no exception guarantees and leaves objects in corrupted states. Figure 19.1: Overview of exception guarantees. At a minimum, professional code should provide basic exception guarantees—it should ensure that if an exception happens, the object’s invariants are maintained, and no memory is leaked. The first piece of code in the assignment operator example above provides

a strong exception guarantee—beyond just promising to respect invariants and not leak memory, this guarantee means that if an exception happens, no side effects will be visible. In our example, the object being assigned to will either be properly assigned to (if no exception happens) or will be completely unchanged (if new fails). An even stronger exception guarantee is the no-throw guarantee (also called the no-fail guarantee), which promises that the code will never throw an exception—if any of the operations it performs fail, it will handle the resulting exception(s). Destructors should always provide a no-throw guarantee. If a class’s destructor does not provide a no-throw guarantee, then any code that creates instances of that class cannot provide any guarantee, as it cannot ensure that the object it created will be properly destroyed. Figure 19.1 summarizes the four standard levels of exception guarantees. The first step in providing any of these guarantees is to understand the exception behavior of the operations that your code uses. In our assignment operator example, the new operation may throw an exception (specifically, std::bad_alloc); however, the other operations involved cannot. As there is only one possible source of failure, we can provide a strong guarantee relatively easily—we perform the operation that might fail before we make any changes to the object. More generally, however, we may need to go to greater lengths to provide exception safety. Consider the following: 1 template<typename T> 2 class MyArray { 3 T * data; 4 size_t size; 5 public: 6 //other methods elided 7 MyArray<T> & operator=(const MyArray<T> & rhs)

8 if (this != &rhs) { 9 T * newData = new T[rhs.size]; 10 for (int i = 0; i < rhs.size; i++) { 11 newData[i] = rhs.data[i]; 12 } 13 delete[] data; 14 data = newData; 15 size = rhs.size; 16 } 17 return *this; 18 } This assignment operator looks quite similar to our strong guarantee assignment operator for IntArrays; however, here we have a templated class that holds any type T. Now, the assignment statement on line 11 might throw an exception—for example T might be IntArray, in which case the assignment uses the operator=, which we just discussed. If the assignment statement on line 11 throws an exception, this assignment operator will leak memory —meaning it provides no exception guarantees. One way we could fix this code would be to catch the exception, clean up our memory allocations, then re- throw the exception: 1 template<typename T> 2 class MyArray { 3 T * data; 4 size_t size; 5 public: 6 //other methods elided 7 MyArray<T> & operator=(const MyArray<T> & rhs) 8 if (this != &rhs) { 9 T * newData = new T[rhs.size]; 10 try { 11 for (int i = 0; i < rhs.size; i++) { 12 newData[i] = rhs.data[i]; 13 } 14 } 15 catch(std::exception & e) { 16 delete[] newData; 17 throw;

18 } 19 delete[] data; 20 data = newData; 21 size = rhs.size; 22 } 23 return *this; 24 } This implementation of the assignment operator provides a strong guarantee if and only if the copy assignment operator for type T provides at least a basic exception guarantee (basic, strong, or no-throw) and if the destructor for type T provides a no-throw guarantee. Otherwise, this code provides no exception guarantees. If the rest of our code is written correctly, these requirements will be met—all code should provide at least a basic guarantee, and destructors should always provide a no-throw guarantee. This approach—inserting try/catch to clean up whenever we might have an exception—works but is not ideal. In fact, cluttering up our code with all of the requisite try/catches goes against one of the reasons we wanted exceptions in the first place: to reduce the clutter from handling errors properly and thoroughly. Fortunately, there are better approaches. 19.7.1 Resource Acquisition Is Initialization In this particular case—where we want a sequence of elements of type T—we could just use a std::vector anyways instead of a dynamically allocated array. The vector will internally have a dynamically allocated array, but the code for it is already written in ways that provide proper exception guarantees. If we have the object directly in the frame (as opposed to a pointer in the frame

to a dynamically allocated object in the heap), then the object’s destructor will be invoked when an exception propagates out of the function, destroying its frame. We can use the fact that destructors are invoked on objects in the frame when the exception destroys the frame to simplify exception safe resource management (whether memory allocation/deallocation or other resources) in the general case. What we need is an object in the local frame that is constructed when the resource is allocated and whose destructor frees that resource (unless we explicitly remove the resource from that object’s control). This design principle is called Resource Acquisition is Initialization (or RAII for short). C++’s STL provides a templated class, std::auto_ptr<T>, which is designed to help write exception safe code with the RAII paradigm. The basic use of the std::auto_ptr<T> template is to initialize it by passing the result of new to its constructor, then make use of the get to obtain the underlying pointer or the overloaded * operator to dereference that pointer. If the auto_ptr is destroyed while it still “owns” the pointer, it will delete the pointer it owns. You can release the pointer from its ownership with the release method, after which it will not destroy it (and get will return NULL). For example (suppose A, B, X, and Y are classes): 1 //an example that does not do anything particularly useful 2 X * someFunction(A & anA, B & aB) { 3 std::auto_ptr<X> myX(new X()); 4 std::auto_ptr<Y> myY(new Y()); 5 aB.someMethod(myX.get()); //someMethod takes an 6 *myY = anA.somethingElse(); //dereference pointer 7 return myX.release(); //remove ownership of pointe 8 } //myY will delete the Y pointer it owns This code provides a basic exception guarantee (as long as everything it calls does too). On line 3, we

allocate memory (which might fail—but that is fine), but then we give ownership of that memory to an auto_ptr. On line 4, we allocate more memory (which also might fail—in which case, the exception destroys the first auto_ptr, freeing the memory we allocated). On the next line of code, we call someMethod on aB, passing in the X * that new X() evaluated to (which is what the auto_ptr is holding). On the next line, we use the overloaded * operator to dereference the pointer owned by the auto_ptr (equivalent to *myY.get()) and store the result of anA.somethingElse() in that box. If either of these methods throws an exception, both auto_ptrs will be destroyed, freeing the corresponding memory. Finally, we release ownership of the pointer in myX and return it. The return value of release is the pointer that was owned by the auto_ptr, but it also makes it so that the auto_ptr owns no pointers. Now when myX is destroyed, it will do nothing, which is good because if it destroyed the pointer it previously owned, the return value of this function would be a dangling pointer. When the function returns, its frame is destroyed, including both auto_ptrs. As we just discussed, myX no longer owns any pointer, so its destructor does nothing. myY still owns a pointer, so it deletes that pointer. We will note that std::auto_ptr does not work with arrays (as it uses delete and not delete[]). The Boost Library (see http://www.boost.org/) provides a boost::interprocess::unique_ptr<T,D> templated class. The second template argument specifies how to delete the owned pointer, making it possible to use it properly with arrays. C++11 adapts this template into std::unique_ptr and deprecates std::auto_ptr (see Section E.6.9).

19.7.2 Exception Safety Idiom: Temp-and- Swap One exception safety idiom is to modify an object by creating a temporary object (of the same type) and then swapping the contents of the newly created temporary object with the original object. This idiom provides a strong exception guarantee (if the swap operation provides at least a strong exception guarantee— preferably, it will provide a no-throw guarantee) as the modifications all occur on the newly created temp object. If anything goes wrong, temp will be destroyed during the destruction of the stack frame, and the original object will remain unchanged. If the modifications complete successfully, then the contents of the modified objects are swapped with the original, which updates the state of the original object and leaves its old state in the temporary object (which will be destroyed at the end of the function). In this idiom, our code might look generally like this: 1 class SomeClass { 2 void makeSomeModifications() { 3 SomeClass temp(*this); 4 //make changes to temp 5 //... 6 std::swap(*this, temp); 7} 8 }; Note that std::swap is a templated function that performs the swap operation using the copy constructor and copy assignment operator of the class involved.5 Accordingly, you cannot use the general std::swap to implement the assignment operator. We can, however, define our own swap operation (which we could probably do in a more efficient manner) and, if we wanted to,

provide it as an explicit specialization6 of the std::swap template. We will note that temp-and-swap is a conceptually simple idiom for providing strong exception guarantees, but it comes at a performance cost—making extra copies and moving data around takes time. These hidden performance costs are a great example of why you should fully understand the entire behavior of every line of code you write. When using temp-and-swap, you should think through how much extra copying and data movement is involved and keep its performance cost in mind as you continue to develop. As with many C++ topics, we could write an entire volume on exception safety, but our purpose here is not to delve deeply into the depths of this topic. Rather, we want you to understand that it is a significant concern in writing truly correct code, give you some basic understanding of the issues involved, and illustrate a few of the basic principles in writing exception-safe code in a reasonable way. If you go on to become a serious C++ programmer, this is a topic you should learn more about from experience and an in-depth C++ book. 19.8 Real C++ As we finish out this part of the book, we want to take a moment to note that C++ is a lot more than just C with classes (or templates). RAII, which we have discussed in this chapter is a very significant part of that difference. An experienced C++ programmer seldom has pointers to dynamically allocated objects, as they violate this principle. Instead, they use RAII to manage their objects. A nice consequence of this design choice is that if your objects do not have pointers to dynamically allocated

objects (or other data that requires special handling for copy/destruction), then you do not have to write any Rule of Three methods (remember: Ro3 does not say you have to always write them—it says if you have to write any you have to write them all)! We will also note that we told you to avoid casting in C, but urge this even more strongly in C++. If you feel like you need to cast in C++, you should know that “C style” casts are legal, but generally not what you want as the other casts let you be more specific about what exactly you are trying to convert. See Section E.4.2 for more on C++’s casting options. As we enter the part of book on data structures, we will, however, manipulate dynamically allocated pointers directly. Part of the reason for this is that we are not going to deeply cover smart pointers7, which would give us a good solution to handling our pointers with RAII. However, you will learn everything important about these data structures, and write acceptable C++ code by obeying the Rule of Three. If you plan to go on to a career involving professional C++ programming, we recommend that you first read Section E.6 to learn about C++11, which has some major changes over C++03. You should then go on to learn about C++14 and C++17. After that, you should read books specifically about C++ (e.g., “Effective C++” and “Effective Modern C++” by Scott Meyers). 19.9 Practice Exercises Selected questions have links to answers in the back of the book. • Question 19.1 : What is “silent failure”? Why is it the worst way for a program to fail?

• Question 19.2 : What is “bulletproof code”? • Question 19.3 : What is errno? • Question 19.4 : What is a C++ exception? What does it mean to “throw” one? What does it mean to “catch” one? • Question 19.5 : When an exception propagates out of a function, what happens to the objects in that function’s frame? • Question 19.6 : When you throw an exception, should you use new to create the exception object? If not, what should you do? • Question 19.7 : When you catch an exception, should you catch a pointer to the exception type, a reference to the exception type, or the value of the exception type? • Question 19.8 : What happens if an exception propagates out of main? • Question 19.9 : What are the four levels of exception guarantees that a function can make? What do each of these exception guarantees mean? What is the minimum level appropriate for professional code? • Question 19.10 : What is RAII? • Question 19.11 : Consider the following code, in particular the function f: 1 template<typename T> 2 class SomeClass { 3 T a; 4 T b; 5 int * ptr; 6 //other methods elided 7 int f(int x) { 8 int * temp = new int[x]; 9 int ans = 0; 10 for (int i = 0; i < x; i++) { 11 T z = a + b; 12 int num = g(z); 13 temp[i] = num; 14 ans += num; 15 }

16 std::swap(temp,ptr); 17 delete[] temp; 18 return ans; 19 } 20 }; Under what conditions (exception guarantees provided by various functions and operators that f calls) would f provide a no-throw exception guarantee? What about a strong exception guarantee? What about a basic guarantee? 18 InheritanceIII Data Structures and Algorithms Generated on Thu Jun 27 15:08:37 2019 by L T XML


Like this book? You can publish your book online for free in a few minutes!
Create your own flipbook