Reading Notes Objective c ++ Item 29 strives for exceptional and secure code.
Abnormal security in a sense is like being pregnant... But think about it. We cannot really discuss reproductive issues before we propose marriage proposal.
Suppose we have a class that represents the GUI menu, which has a background image. This class will be used in a multi-threaded environment, so mutex is required for concurrency control.
1 class PrettyMenu { 2 public: 3 ... 4 void changeBackground(std::istream& imgSrc); // change background 5 ... // image 6 7 private: 8 Mutex mutex; // mutex for this object 9 10 Image *bgImage; // current background image11 12 13 14 int imageChanges; // # of times image has been changed15 16 };
Let's take a look at the possible implementation of the changeBackground function of a PrettyMenu:
1 void PrettyMenu::changeBackground(std::istream& imgSrc) 2 { 3 4 lock(&mutex); // acquire mutex (as in Item 14) 5 6 delete bgImage; // get rid of old background 7 8 9 ++imageChanges; // update image change count10 bgImage = new Image(imgSrc); // install new background11 12 unlock(&mutex); // release mutex13 14 }
1. Features of functions with exceptional security
From the perspective of exception security, this function is very bad. For exception security, there are two requirements. The above implementation does not meet any one.
When an exception is thrown, the function for exception security:
- Resources are not disclosed. The above code will not pass this test, because if the "new Image (imgSrc)" expression produces an exception, the unlock will never be called and the current thread will always have a lock.
- The data structure cannot be damaged. If "new Image (imgSrc)" throws an exception, bgImage points to a destroyed object. In addition, imageChanges is added, but the actual situation is that the new image is not loaded. (In another aspect, the old image is completely cleared, so I guess you will argue that the image has been "modified)
It is easy to handle resource leaks because Item 13 explains how to use objects to manage resources. Item 14 introduces the Lock class to ensure that mutex can be released in Real Time:
1 void PrettyMenu::changeBackground(std::istream& imgSrc)2 {3 Lock ml(&mutex); // from Item 14: acquire mutex and4 // ensure its later release5 delete bgImage;6 ++imageChanges;7 bgImage = new Image(imgSrc);8 }
One of the major advantages of using resource management classes like Lock is that it usually makes functions shorter. Let's see why the unlock call is no longer needed? As a general rule, the fewer the code, the better, because the possibility of errors and understanding errors becomes lower when the code is modified.
2. Three Levels of exceptional security
Next, let's take a look at the problem of data structure corruption. Here we need to make a choice, but before we can make a choice, we must compare the terms that define these choices.
Functions with exceptional security provide one of the following three guarantees:
- Basic Guarantee (Basic guarantee)When such a function throws an exception, everything in the program remains in the valid state. No object or data is damaged, and all objects or data are kept in an internal consistent State (for example, constraints of all classes continue to be met ). However, the correct state of the program may not be predicted. For example, changeBackground can be implemented. Once an exception is thrown, The PrettyMenu object can continue to have the old background image, but the customer cannot predict which one is supported. (To find out the image, you need to call some member functions to tell them which background image is used)
- Strong guarantee (Strong guarantee). When such a function is thrown, the state of the program will not be changed. The call to these functions is atomic, meaning that if the call succeeds, it will be completely successful, but if it fails, the state of the program will be the same as if it was not called.
It is easier to use functions that provide strong guarantee than to provide functions that only provide basic guarantee, because after calling a function that provides strong guarantee, there may be only two Program states: the status after the function is correctly executed or before the function is called. If an exception is thrown when you call a function that only provides the basic guarantee, the program can enter any valid state.
- No exception guarantee(Nothrow guarantee). Such functions will never throw an exception because they always do what they promise. All operations performed on the internal creation type are normal. This is a key building foundation for exceptional security code.
The empty exception specification function is considered to have no exception, which may seem reasonable, but in fact it is not. For example, consider the following functions:
1 int doSomething() throw(); // note empty exception spec.
This does not mean that doSomething will never throw an exception. It means that if soSomething throws an exception, it will be a serious error and an unexpected function will be called. In fact, doSomething does not provide any exception security guarantee. The declaration of this function (including exception details if any) does not tell you whether the function is correct, portable, or highly efficient, it does not provide you with any exception security guarantee. All these features are determined by the implementation of functions, rather than declarations.
The exception-safe code must provide one of the above three guarantees. If it is not provided, it is not exceptionally secure. Your choice determines which guarantee is provided for your implemented function. In addition to providing exception security protection when dealing with old codes with exceptions and insecure codes, the abnormal and insecure code is required only in the following situations: when your team conducts demand analysis, there is a need to expose resources and run programs on the broken data structure.
As a standard, it is a practical idea to provide the strongest exceptional security assurance. From the perspective of exception security, functions that do not throw exceptions are perfect. However, in C ++, it is difficult to call functions that may throw exceptions. Anything that uses dynamic memory allocation (for example, all STL containers) will throw a bad_alloc exception (Item 49) If there is not enough memory available for allocation ). It is better to provide functions that do not throw exceptions.Basic GuaranteeAndStrong guaranteeBetween.
3. Two Methods of exception security are provided. 3.1 use smart pointers.
It is not difficult to provide strong guarantees for changeBackground. First, we extract the bgImage data member type of the PrettyMenu from the built-in Image * pointer.Replaced by a SMART Resource Management pointer(See Item 13 ). To be honest, this is definitely a good way to prevent resource leaks. The fact that it provides us with strong exception security guarantee is simply a further enhancement to the discussion in Item 13 (using object management resources is the basis of a good design. In the following code, I will show how to use tr1: shared_ptr, because it is more intuitive and popular to use tr1: shared_ptr when copying data.
Second,WeThe statements in changeBackground are reordered.To add imageChnages only when the image is modified. As a general principle, if the State of an object is not modified, it indicates that something has not happened.
The final code is as follows:
1 class PrettyMenu { 2 ... 3 std::tr1::shared_ptr<Image> bgImage; 4 ... 5 }; 6 void PrettyMenu::changeBackground(std::istream& imgSrc) 7 { 8 Lock ml(&mutex); 9 bgImage.reset(new Image(imgSrc)); // replace bgImage’s internal10 // pointer with the result of the11 // “new Image” expression12 ++imageChanges;13 }
Note that you do not need to manually delete the old image because it is processed internally by the smart pointer. In addition, the destruction operation only occurs when the new image is successfully created. More precisely, the tr1: shared_ptr: reset function is called only when the parameter (new Image (imgSrc) result) is successfully created. Delete is used only within the reset function. Therefore, if a reset is not called, delete will never be executed. Note that the use of resource management objects reduces the length of changeBackground again.
As I said, the above two changes are sufficient to provide strong exception security assurance for changeBackground. In addition, the imgSrc parameter is missing. If an exception is thrown by the Image constructor, the read Mark of the input stream may be moved, which changes the status and is visible to the subsequent running of the program. If changeBackground does not handle this problem, it can only provide basic exception security assurance.
3.2 copy and exchange
Put the above problem aside. We assume that changeBackground can provide strong exception security assurance. (You should be able to come up with a good solution to provide strong exception security, maybe you can change the parameter type from istream to the name of the file containing image data .)There is a general design strategy that can also provide strong assurance, it is very important to be familiar with it. This policy is called "copy and Exchange "(Copy and swap). It is very simple: first, make a copy of the object you want to modify, and then make all the necessary changes on the copy. If any modification operation throws an exception, the source object remains unchanged. Once the modification is complete, the source object and the modified object will not throw an exception Exchange (Item 25 ).
This will usually put the real object data into a separate implementation object, and then provide a pointer to this implementation object. That is, the pointer to the implementation (pimpl idiom), which will be described in detail in Item 31. The implementation of PrerttyMenu is as follows:
1 struct PMImpl { // PMImpl = “PrettyMenu 2 std::tr1::shared_ptr<Image> bgImage; // Impl.”; see below for 3 int imageChanges; // why it’s a struct 4 }; 5 class PrettyMenu { 6 ... 7 private: 8 Mutex mutex; 9 std::tr1::shared_ptr<PMImpl> pImpl;10 };11 void PrettyMenu::changeBackground(std::istream& imgSrc)12 {13 using std::swap; // see Item 2514 Lock ml(&mutex); // acquire the mutex15 16 std::tr1::shared_ptr<PMImpl> // copy obj. data17 18 pNew(new PMImpl(*pImpl)); 19 20 21 pNew->bgImage.reset(new Image(imgSrc)); // modify the copy22 ++pNew->imageChanges;23 24 swap(pImpl, pNew); // swap the new25 // data into place26 27 } // release the mutex
In this example, I chose to define PMImpl as a struct rather than a class, because the encapsulation of PrettyMenu is guaranteed by the private nature of pImpl. Defining PMImpl as a class is at least as good as a struct, though inconvenient. If needed, PMImpl can be placed in PrettyMenu, but packaging is a concern.
The copy and exchange policy is an excellent method for handling object state issues (the State changes either completely or unchanged). However, in general, it cannot ensure that all functions are highly exceptional and secure. To learn why, consider an abstraction of changeBackground, someFunc. This function uses a copy and exchange policy, but it also contains calls to the other two functions, f1 and f2:
1 void someFunc() 2 { 3 4 ... // make copy of local state 5 6 f1(); 7 8 f2(); 9 10 11 12 ... // swap modified state into place13 14 }
Here, if f1 or f2 is not highly exceptional and secure, someFunc is hard to be highly exceptional. For example, if f1 only provides the basic guarantee. If someFunc needs to provide strong guarantee, you must write code to determine the state of the entire program before calling f1, and then capture all exceptions in f1. If an exception occurs, the original state is restored.
Even if f1 and f2 are highly exceptional and secure, the situation has not changed. If the program status changes after f1 is run, if f2 throws an exception, the program status is different from that before someFunc is called, even if f2 does not modify anything.
4. Two situations of strong exception security cannot be provided
When function operations only affect the local State (for example, someFunc only affects the State of the object that calls this function), it is relatively easy to provide strong exception security. It is quite difficult to provide strong guarantees when functions also have side effects on non-local data. For example, if the side effect of calling f1 is that the database is modified, it is difficult for someFunc to provide high exception security. Generally, there is no way to roll back submitted database changes. Other database clients may have seen the new status of the database.
Even if you want to provide a strong exception security guarantee, the above problems will also prevent you. Another problem is efficiency. The key point of copying and switching is to first modify the object copy, and then exchange the source data and the modified data without exception. This requires a copy of each object to be modified, which requires time and space. You may not be able or unwilling to provide these resources for it. Everyone wants to ensure strong exceptional security. You should provide it in practice, but it is practical if it is not 100%.
5. At least provide basic exception security for the code (except for legacy code)
If not, you must provide basic assurance. In practice, you may find that you can provide strong guarantees for some functions, but the overhead in efficiency and complexity makes them no longer practical. As long as you make efforts to provide functions with strong exception security guarantee, no one will criticize you for providing the basic guarantee. For many functions, basic guarantee is the most reasonable choice.
If you implement a function without providing any exceptional security guarantee, things will be different, because you will continue to be guilty until it proves that you are innocent. Therefore, you should implement exceptional security code. But you may be in conflict. Consider the implementation of someFunc, which calls the f1 and f2 functions. Assume that f2 does not provide exception security assurance, nor does it provide basic assurance. This means that if an exception is thrown inside f2, resource leakage may occur, and damaged data structures may occur. For example, ordered arrays are no longer ordered, objects transmitted from one data structure to another are lost. SomeFunc does not have any way to compensate for these issues. If someFunc calls a function that does not provide exception security, someFunc cannot provide any guarantee.
Let's go back to the pregnancy topic. A woman is either pregnant or not pregnant. It's impossible to partially get pregnant. Similarly, a software system is either exceptionally secure or not. There are no abnormal and secure systems. If a system contains a function that does not provide exceptional security, the system is not so secure as to call this function, resulting in resource leakage and data structure destruction. Unfortunately, many legacy C ++ code is not implemented as exceptional security, so too many systems are not so secure today.
There is no reason to maintain this status. Therefore, when writing new code or modifying existing code, you need to carefully consider how to make it abnormal and secure. First, use object to manage resources (Item 13), which can prevent resource leakage. Then, select the most practical and powerful one for each function from the three exception security guarantees. You can barely accept the non-security guarantee only when you have no choice to call the legacy code. Record your decisions for function users and future maintenance personnel in the document. Function exception security guarantee is a visible part of the interface, so when you choose the exception security guarantee part, you should be as cautious as you choose other aspects of the function interface.
The goto statement was considered a good practice 40 years ago. Now we are trying to implement structured control flow (structured control flows ). 20 years ago, global data access was considered a good practice. Now we try to encapsulate the data. 10 years ago, it was considered a good practice to implement functions that do not have to consider the impact of exceptions. Now we try to write exceptional security code.
Keep pace with the times. Learn to be old.
6. Summary
- Abnormal security functions do not cause resource leakage or damage to the data structure, even if an exception is thrown. Such a function provides three basic guarantees: strong and no exception thrown.
- Strong assurance is usually implemented through copying and switching, but it is impractical to provide strong assurance for all functions.
- The strongest exception security guarantee provided by a function is no better than the weakest exception security guarantee provided by the function it calls.