<Item29> Strive for exception-safe code.
1、如下面的代码
class PrettyMenu { public: ... void changeBackground(std::istream& imgSrc); // change background ... // image private: Mutex mutex; // mutex for this object Image *bgImage; // current background image int imageChanges; // # of times image has been changed };
void PrettyMenu::changeBackground(std::istream& imgSrc) { lock(&mutex); // acquire mutex (as in Item 14) delete bgImage; // get rid of old background ++imageChanges; // update image change count bgImage = new Image(imgSrc); // install new background unlock(&mutex); // release mutex }
异常安全的函数需要满足两个要求,changeBackground一个也没满足
-
Leak no resources. The code above fails this test, because if the "new Image(imgSrc)" expression yields an exception, the call to unlock never gets executed, and the mutex is held forever.
-
Don't allow data structures to become corrupted. If "new Image(imgSrc)" throws, bgImage is left pointing to a deleted object. In addition, imageChanges has been incremented, even though it's not true that a new image has been installed. (On the other hand, the old image has definitely been eliminated, so I suppose you could argue that the image has been "changed.")
2、Addressing the resource leak issue is easy, because Item 13 explains how to use objects to manage resources, and Item 14 introduces the Lock class as a way to ensure that mutexes are released in a timely fashion:
void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock ml(&mutex); // from Item 14: acquire mutex and // ensure its later release delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); }
One of the best things about resource management classes like Lock is that they usually make functions shorter. See how the call to unlock is no longer needed? As a general rule, less code is better code, because there's less to go wrong and less to misunderstand when making changes.
3、对于数据冲突,异常安全的函数提供了三种类型的保障,异常安全的代码应该提供下面的三种保障之一,否则不能算是异常安全的代码
-
-
Functions offering the basic guarantee promise that if an exception is thrown, everything in the program remains in a valid state. No objects or data structures become corrupted, and all objects are in an internally consistent state (e.g., all class invariants are satisfied). However, the exact state of the program may not be predictable. For example, we could write changeBackground so that if an exception were thrown, the PrettyMenu object might continue to have the old background image, or it might have some default background image, but clients wouldn't be able to predict which. (To find out, they'd presumably have to call some member function that would tell them what the current background image was.)
-
Functions offering the strong guarantee promise that if an exception is thrown, the state of the program is unchanged. Calls to such functions are atomic in the sense that if they succeed, they succeed completely, and if they fail, the program state is as if they'd never been called.
Working with functions offering the strong guarantee is easier than working with functions offering only the basic guarantee, because after calling a function offering the strong guarantee, there are only two possible program states: as expected following successful execution of the function, or the state that existed at the time the function was called. In contrast, if a call to a function offering only the basic guarantee yields an exception, the program could be in any valid state.
-
Functions offering the nothrow guarantee promise never to throw exceptions, because they always do what they promise to do. All operations on built-in types (e.g., ints, pointers, etc.) are nothrow (i.e., offer the nothrow guarantee). This is a critical building block of exception-safe code.
It might seem reasonable to assume that functions with an empty exception specification are nothrow, but this isn't necessarily true. For example, consider this function:
int doSomething() throw(); // note empty exception spec.
This doesn't say that doSomething will never throw an exception; it says that if doSomething tHRows an exception, it's a serious error, and the unexpected function should be called.[1] In fact, doSomething may not offer any exception guarantee at all. The declaration of a function (including its exception specification, if it has one) doesn't tell you whether a function is correct or portable or efficient, and it doesn't tell you which, if any, exception safety guarantee it offers, either. All those characteristics are determined by the function's implementation, not its declaration.在异常处理中,如果函数实际抛出的异常类型与我们的异常类型规格说明不一致,这时会调用特殊函数unexpected();我们通过set_unexpected()来设置自己的处理函数。
-
4、As a general rule, you want to offer the strongest guarantee that's practical. From an exception safety point of view, nothrow functions are wonderful, but it's hard to climb out of the C part of C++ without calling functions that might throw. Anything using dynamically allocated memory (e.g., all STL containers) typically throws a bad_alloc exception if it can't find enough memory to satisfy a request (see Item 49). Offer the nothrow guarantee when you can, but for most functions, the choice is between the basic and strong guarantees.
5、First, we change the type of PrettyMenu's bgImage data member from a built-in Image* pointer to one of the smart resource-managing pointers described in Item 13.Second, As a general rule, it's a good policy not to change the status of an object to indicate that something has happened until something actually has. 比如下面的代码则是strong guarantee 并且代码行数减少了
class PrettyMenu { ... std::tr1::shared_ptr<Image> bgImage; ... }; void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock ml(&mutex); bgImage.reset(new Image(imgSrc)); // replace bgImage's internal // pointer with the result of the // "new Image" expression ++imageChanges; }
美中不足的是 If the Image constructor throws an exception, it's possible that the read marker for the input stream has been moved, and such movement would be a change in state visible to the rest of the program. Until changeBackground addresses that issue, it offers only the basic exception safety guarantee.
6、There is a general design strategy that typically leads to the strong guarantee, and it's important to be familiar with it. The strategy is known as "copy and swap." In principle, it's very simple. Make a copy of the object you want to modify, then make all needed changes to the copy. If any of the modifying operations throws an exception, the original object remains unchanged. After all the changes have been successfully completed, swap the modified object with the original in a non-throwing operation.This is usually implemented by putting all the per-object data from the "real" object into a separate implementation object, then giving the real object a pointer to its implementation object. This is often known as the "pimpl idiom," and Item 31 describes it in some detail. For PrettyMenu, it would typically look something like this:
struct PMImpl { // PMImpl = "PrettyMenu std::tr1::shared_ptr<Image> bgImage; // Impl."; see below for int imageChanges; // why it's a struct }; class PrettyMenu { ... private: Mutex mutex; std::tr1::shared_ptr<PMImpl> pImpl; }; void PrettyMenu::changeBackground(std::istream& imgSrc) { using std::swap; // see Item 25 Lock ml(&mutex); // acquire the mutex std::tr1::shared_ptr<PMImpl> // copy obj. data pNew(new PMImpl(*pImpl)); pNew->bgImage.reset(new Image(imgSrc)); // modify the copy ++pNew->imageChanges; swap(pImpl, pNew); // swap the new // data into place } // release the mutex
7、The copy-and-swap strategy is an excellent way to make all-or-nothing changes to an object's state, but, in general, it doesn't guarantee that the overall function is strongly exception-safe. To see why, consider an abstraction of changeBackground, someFunc, that uses copy-and-swap, but that includes calls to two other functions, f1 and f2:
void someFunc() { ... // make copy of local state f1(); f2(); ... // swap modified state into place }
It should be clear that if f1 or f2 is less than strongly exception-safe, it will be hard for someFunc to be strongly exception-safe. For example, suppose that f1 offers only the basic guarantee. For someFunc to offer the strong guarantee, it would have to write code to determine the state of the entire program prior to calling f1, catch all exceptions from f1, then restore the original state.
Things aren't really any better if both f1 and f2 are strongly exception safe. After all, if f1 runs to completion, the state of the program may have changed in arbitrary ways, so if f2 then throws an exception, the state of the program is not the same as it was when someFunc was called, even though f2 didn't change anything.
The problem is side effects. As long as functions operate only on local state (e.g., someFunc affects only the state of the object on which it's invoked), it's relatively easy to offer the strong guarantee. When functions have side effects on non-local data, it's much harder. If a side effect of calling f1, for example, is that a database is modified, it will be hard to make someFunc strongly exception-safe. There is, in general, no way to undo a database modification that has already been committed; other database clients may have already seen the new state of the database.
8、Issues such as these can prevent you from offering the strong guarantee for a function, even though you'd like to. Another issue is efficiency(复制交换影响效率).The strong guarantee is highly desirable, and you should offer it when it's practical, but it's not practical 100% of the time.When it's not, you'll have to offer the basic guarantee.For many functions, the basic guarantee is a perfectly reasonable choice.
9、Things are different if you write a function offering no exception-safety guarantee at all, because in this respect it's reasonable to assume that you're guilty until proven innocent. You should be writing exception-safe code. But you may have a compelling defense. Consider again the implementation of someFunc that calls the functions f1 and f2. Suppose f2 offers no exception safety guarantee at all, not even the basic guarantee. That means that if f2 emits an exception, the program may have leaked resources inside f2. It means that f2 may have corrupted data structures, e.g., sorted arrays might not be sorted any longer, objects being transferred from one data structure to another might have been lost, etc. There's no way that someFunc can compensate for those problems. If the functions someFunc calls offer no exception-safety guarantees, someFunc itself can't offer any guarantees.
10、Similarly, a software system is either exception-safe or it's not. There's no such thing as a partially exception-safe system. If a system has even a single function that's not exception-safe, the system as a whole is not exception-safe, because calls to that one function could lead to leaked resources and corrupted data structures.写出完全异常安全的C++程序几乎是不可能的但是以异常安全为目标去写程序总是好的,即使最终没有得到异常安全的程序,但是通过这个过程减少了bug的数量。
11、When writing new code or modifying existing code, think carefully about how to make it exception-safe. Begin by using objects to manage resources. (Again, see Item 13.) That will prevent resource leaks. Follow that by determining which of the three exception safety guarantees is the strongest you can practically offer for each function you write, settling for no guarantee only if calls to legacy code leave you no choice. Document your decisions, both for clients of your functions and for future maintainers. A function's exception-safety guarantee is a visible part of its interface, so you should choose it as deliberately as you choose all other aspects of a function's interface.
12、Forty years ago, goto-laden code was considered perfectly good practice. Now we strive to write structured control flows. Twenty years ago, globally accessible data was considered perfectly good practice. Now we strive to encapsulate data. Ten years ago, writing functions without thinking about the impact of exceptions was considered perfectly good practice. Now we strive to write exception-safe code.Time goes on. We live. We learn.
13、Things to Remember
-
Exception-safe functions leak no resources and allow no data structures to become corrupted, even when exceptions are thrown. Such functions offer the basic, strong, or nothrow guarantees.
-
The strong guarantee can often be implemented via copy-and-swap, but the strong guarantee is not practical for all functions.
-
A function can usually offer a guarantee no stronger than the weakest guarantee of the functions it calls.
<Item 30> Understand the ins and outs of inlining.
14、Inline functions — what a wonderful idea! They look like functions, they act like functions, they're ever so much better than macros (see Item 2), and you can call them without having to incur the overhead of a function call.Compiler optimizations are typically designed for stretches of code that lack function calls, so when you inline a function, you may enable compilers to perform context-specific optimizations on the body of the function. Most compilers never perform such optimizations on "outlined" function calls.
15、In programming, however, as in life, there is no free lunch, and inline functions are no exception. The idea behind an inline function is to replace each call of that function with its code body, and it doesn't take a Ph.D. in statistics to see that this is likely to increase the size of your object code. On machines with limited memory, overzealous inlining can give rise to programs that are too big for the available space. Even with virtual memory, inline-induced code bloat can lead to additional paging, a reduced instruction cache hit rate, and the performance penalties that accompany these things.On the other hand, if an inline function body is very short, the code generated for the function body may be smaller than the code generated for a function call. If that is the case, inlining the function may actually lead to smaller object code and a higher instruction cache hit rate!
16、Bear in mind that inline is a request to compilers, not a command. The request can be given implicitly or explicitly. The implicit way is to define a function inside a class definition:
class Person { public: ... int age() const { return theAge; } // an implicit inline request: age is ... // defined in a class definition private: int theAge; };
Such functions are usually member functions, but Item 46 explains that friend functions can also be defined inside classes. When they are, they're also implicitly declared inline.
17、The explicit way to declare an inline function is to precede its definition with the inline keyword. For example, this is how the standard max template (from <algorithm>) is often implemented:
template<typename T> // an explicit inline inline const T& std::max(const T& a, const T& b) // request: std::max is { return a < b ? b : a; } // preceded by "inline"
The fact that max is a template brings up the observation that both inline functions and templates are typically defined in header files. This leads some programmers to conclude that function templates must be inline. This conclusion is both invalid and potentially harmful, so it's worth looking into it a bit.
18、Inline functions must typically be in header files, because most build environments do inlining during compilation. In order to replace a function call with the body of the called function, compilers must know what the function looks like. (Some build environments can inline during linking, and a few — e.g., managed environments based on the .NET Common Language Infrastructure (CLI) — can actually inline at runtime. Such environments are the exception, however, not the rule. Inlining in most C++ programs is a compile-time activity.)
Templates are typically in header files, because compilers need to know what a template looks like in order to instantiate it when it's used. (Again, this is not universal. Some build environments perform template instantiation during linking. However, compile-time instantiation is more common.)
19、Most compilers refuse to inline functions they deem too complicated (e.g., those that contain loops or are recursive), and all but the most trivial calls to virtual functions defy inlining.virtual意味着在运行的时候才会知道具体要调用那个函数,而inline一般是在编译器进行替换,因此不能实现。whether a given inline function is actually inlined depends on the build environment you're using — primarily on the compiler. Fortunately, most compilers have a diagnostic level that will result in a warning (see Item 53) if they fail to inline a function you've asked them to
20、Sometimes compilers generate a function body for an inline function even when they are perfectly willing to inline the function. For example, if your program takes the address of an inline function, compilers must typically generate an outlined function body for it. How can they come up with a pointer to a function that doesn't exist? Coupled with the fact that compilers typically don't perform inlining across calls through function pointers, this means that calls to an inline function may or may not be inlined, depending on how the calls are made:
inline void f() {...} // assume compilers are willing to inline calls to f void (*pf)() = f; // pf points to f ... f(); // this call will be inlined, because it's a "normal" call pf(); // this call probably won't be, because it's through // a function pointer
21、The specter of un-inlined inline functions can haunt you even if you never use function pointers, because programmers aren't necessarily the only ones asking for pointers to functions. Sometimes compilers generate out-of-line copies of constructors and destructors so that they can get pointers to those functions for use during construction and destruction of objects in arrays.
22、In fact, constructors and destructors are often worse candidates for inlining than a casual examination would indicate. For example, consider the constructor for class Derived below:多个相同类型的对象成员时,可能会产生多个构造函数的copy
23、Library designers must evaluate the impact of declaring functions inline, because it's impossible to provide binary upgrades to the client visible inline functions in a library. In other words, if f is an inline function in a library, clients of the library compile the body of f into their applications. If a library implementer later decides to change f, all clients who've used f must recompile. This is often undesirable. On the other hand, if f is a non-inline function, a modification to f requires only that clients relink. This is a substantially less onerous burden than recompiling and, if the library containing the function is dynamically linked, one that may be absorbed in a way that's completely transparent to clients.
24、For purposes of program development, it is important to keep all these considerations in mind, but from a practical point of view during coding, one fact dominates all others: most debuggers have trouble with inline functions. This should be no great revelation. How do you set a breakpoint in a function that isn't there? Although some build environments manage to support debugging of inlined functions, many environments simply disable inlining for debug builds.
25、This leads to a logical strategy for determining which functions should be declared inline and which should not. Initially, don't inline anything, or at least limit your inlining to those functions that must be inline (see Item 46) or are truly trivial (such as Person::age on page 135). By employing inlines cautiously, you facilitate your use of a debugger, but you also put inlining in its proper place: as a hand-applied optimization. Don't forget the empirically determined rule of 80-20, which states that a typical program spends 80% of its time executing only 20% of its code. It's an important rule, because it reminds you that your goal as a software developer is to identify the 20% of your code that can increase your program's overall performance. You can inline and otherwise tweak your functions until the cows come home, but it's wasted effort unless you're focusing on the right functions.
26、Things to Remember
-
Limit most inlining to small, frequently called functions. This facilitates debugging and binary upgradability, minimizes potential code bloat, and maximizes the chances of greater program speed.
-
Don't declare function templates inline just because they appear in header files.
<Item31> Minimize compilation dependencies between files
27、A class definition specifies not only a class interface but also a fair number of implementation details.但是如下前置声明的代码则存在两个问题
namespace std { class string; // forward declaration (an incorrect } // one — see below) class Date; // forward declaration class Address; // forward declaration class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; ... };
首先:There are two problems with this idea. First, string is not a class, it's a typedef (for basic_string<char>). As a result, the forward declaration for string is incorrect. The proper forward declaration is substantially more complex, because it involves additional templates. That doesn't matter, however, because you shouldn't try to manually declare parts of the standard library. Instead, simply use the proper #includes and be done with it. Standard headers are unlikely to be a compilation bottleneck, especially if your build environment allows you to take advantage of precompiled headers. If parsing standard headers really is a problem, you may need to change your interface design to avoid using the parts of the standard library that give rise to the undesirable #includes.
另外:The second (and more significant) difficulty with forward-declaring everything has to do with the need for compilers to know the size of objects during compilation.This question fails to arise in languages like Smalltalk and Java, because, when an object is defined in such languages, compilers allocate only enough space for a pointer to an object. C++可以通过pimpl idiom ("pointer to implementation")技术模拟实现,如下代码With this design, clients of Person are divorced from the details of dates, addresses, and persons. The implementations of those classes can be modified at will, but Person clients need not recompile. In addition, because they're unable to see the details of Person's implementation, clients are unlikely to write code that somehow depends on those details. This is a true separation of interface and implementation.
#include <string> // standard library components // shouldn't be forward-declared #include <memory> // for tr1::shared_ptr; see below class PersonImpl; // forward decl of Person impl. class class Date; // forward decls of classes used in class Address; // Person interface
class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; ... private: // ptr to implementation; std::tr1::shared_ptr<PersonImpl> pImpl; // see Item 13 for info on }; // std::tr1::shared_ptr
28、The key to this separation is replacement of dependencies on definitions with dependencies on declarations. That's the essence of minimizing compilation dependencies: make your header files self-sufficient whenever it's practical, and when it's not, depend on declarations in other files, not definitions. Everything else flows from this simple design strategy. Hence:
-
Avoid using objects when object references and pointers will do. You may define references and pointers to a type with only a declaration for the type. Defining objects of a type necessitates the presence of the type's definition.
-
Depend on class declarations instead of class definitions whenever you can. Note that you never need a class definition to declare a function using that class, not even if the function passes or returns the class type by value。在client调用函数前,对应的class定义肯定是可见的。By moving the onus of providing class definitions from your header file of function declarations to clients' files containing function calls, you eliminate artificial client dependencies on type definitions they don't really need.
-
Provide separate header files for declarations and definitions. In order to facilitate adherence to the above guidelines, header files need to come in pairs: one for declarations, the other for definitions. These files must be kept consistent, of course. If a declaration is changed in one place, it must be changed in both. As a result, library clients should always #include a declaration file instead of forward-declaring something themselves, and library authors should provide both header files. For example, the Date client wishing to declare today and clearAppointments shouldn't manually forward-declare Date as shown above. Rather, it should #include the appropriate header of declarations:
#include "datefwd.h" // header file declaring (but not // defining) class Date Date today(); // as before void clearAppointments(Date d);
The name of the declaration-only header file "datefwd.h" is based on the header <iosfwd> from the standard C++ library (see Item 54). <iosfwd> contains declarations of iostream components whose corresponding definitions are in several different headers, including <sstream>, <streambuf>, <fstream>, and <iostream>.
<iosfwd> is instructive for another reason, and that's to make clear that the advice in this Item applies as well to templates as to non-templates. Although Item 30 explains that in many build environments, template definitions are typically found in header files, some build environments allow template definitions to be in non-header files, so it still makes sense to provide declaration-only headers for templates. <iosfwd> is one such header.
29、C++ also offers the export keyword to allow the separation of template declarations from template definitions. Unfortunately, compiler support for export is scanty, and real-world experience with export is scantier still. As a result, it's too early to say what role export will play in effective C++ programming.
30、Classes like Person that employ the pimpl idiom are often called Handle classes. Lest you wonder how such classes actually do anything, one way is to forward all their function calls to the corresponding implementation classes and have those classes do the real work. For example, here's how two of Person's member functions could be implemented:
#include "Person.h" // we're implementing the Person class, // so we must #include its class definition #include "PersonImpl.h" // we must also #include PersonImpl's class // definition, otherwise we couldn't call // its member functions; note that // PersonImpl has exactly the same // member functions as Person — their // interfaces are identical Person::Person(const std::string& name, const Date& birthday, const Address& addr) : pImpl(new PersonImpl(name, birthday, addr)) {} std::string Person::name() const { return pImpl->name(); }
31、An alternative to the Handle class approach is to make Person a special kind of abstract base class called an Interface class. The purpose of such a class is to specify an interface for derived classes (see Item 34). As a result, it typically has no data members, no constructors, a virtual destructor (see Item 7), and a set of pure virtual functions that specify the interface.Interface classes are akin to Java's and .NET's Interfaces, but C++ doesn't impose the restrictions on Interface classes that Java and .NET impose on Interfaces. Neither Java nor .NET allow data members or function implementations in Interfaces, for example, but C++ forbids neither of these things. C++'s greater flexibility can be useful. As Item 36 explains, the implementation of non-virtual functions should be the same for all classes in a hierarchy, so it makes sense to implement such functions as part of the Interface class that declares them.An Interface class for Person could look like this:
class Person { public: virtual ~Person(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; virtual std::string address() const = 0; ... };
Clients of this class must program in terms of Person pointers and references, because it's not possible to instantiate classes containing pure virtual functions. (It is, however, possible to instantiate classes derived from Person — see below.) Like clients of Handle classes, clients of Interface classes need not recompile unless the Interface class's interface is modified.
32、actually instantiated. Such functions are typically called factory functions (see Item 13) or virtual constructors. They return pointers (preferably smart pointers — see Item 18) to dynamically allocated objects that support the Interface class's interface. Such functions are often declared static inside the Interface class:
class Person { public: ... static std::tr1::shared_ptr<Person> // return a tr1::shared_ptr to a new create(const std::string& name, // Person initialized with the const Date& birthday, // given params; see Item 18 for const Address& addr); // why a tr1::shared_ptr is returned ... };
Clients use them like this:
std::string name; Date dateOfBirth; Address address; ... // create an object supporting the Person interface std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address)); ... std::cout << pp->name() // use the object via the << " was born on " // Person interface << pp->birthDate() << " and now lives at " << pp->address(); ... // the object is automatically // deleted when pp goes out of // scope — see Item 13
33、实现接口类的两种机制:it inherits its interface specification from the Interface class (Person), then it implements the functions in the interface. A second way to implement an Interface class involves multiple inheritance, a topic explored in Item 40.
34、Handle classes and Interface classes decouple interfaces from implementations, thereby reducing compilation dependencies between files.但是代价是: it costs you some speed at runtime, plus some additional memory per object.接口和实现分离对于模块化程序设计非常重要,很多时候所带来的运行时代价是可以承受的。
35、In the case of Handle classes, member functions have to go through the implementation pointer to get to the object's data. That adds one level of indirection per access. And you must add the size of this implementation pointer to the amount of memory required to store each object. Finally, the implementation pointer has to be initialized (in the Handle class's constructors) to point to a dynamically allocated implementation object, so you incur the overhead inherent in dynamic memory allocation (and subsequent deallocation) and the possibility of encountering bad_alloc (out-of-memory) exceptions.
For Interface classes, every function call is virtual, so you pay the cost of an indirect jump each time you make a function call (see Item 7). Also, objects derived from the Interface class must contain a virtual table pointer (again, see Item 7). This pointer may increase the amount of memory needed to store an object, depending on whether the Interface class is the exclusive source of virtual functions for the object.
Finally, neither Handle classes nor Interface classes can get much use out of inline functions. Item 30 explains why function bodies must typically be in header files in order to be inlined, but Handle and Interface classes are specifically designed to hide implementation details like function bodies.
36、It would be a serious mistake, however, to dismiss Handle classes and Interface classes simply because they have a cost associated with them. So do virtual functions, and you wouldn't want to forgo those, would you? (If so, you're reading the wrong book.) Instead, consider using these techniques in an evolutionary manner. Use Handle classes and Interface classes during development to minimize the impact on clients when implementations change. Replace Handle classes and Interface classes with concrete classes for production use when it can be shown that the difference in speed and/or size is significant enough to justify the increased coupling between classes.
37、Things to Remember
-
The general idea behind minimizing compilation dependencies is to depend on declarations instead of definitions. Two approaches based on this idea are Handle classes and Interface classes.
-
Library header files should exist in full and declaration-only forms. This applies regardless of whether templates are involved.