NIUCLOUD是一款SaaS管理后台框架多应用插件+云编译。上千名开发者、服务商正在积极拥抱开发者生态。欢迎开发者们免费入驻。一起助力发展! 广告
# Chapter 18. Tools for Large Programs ## 18.1 Exception Handling Exception handling allows independently developed parts of a program to communicate about and handle problems that arise at run time. ### 18.1.1 Throwing an Exception In C++, an exception is raised by throwing an expression. If the throw appears inside a try block, the `catch` clauses associated with that `try` are examined. If matching `catch` is found, the exception is handled by that `catch`. Otherwise, if the `try` was itself nested inside another `try`, the search continues through the `catch` clauses of the enclosing `trys`. If no matching `catch` is found, the current function is exited, and the search continues in the calling function. This process, known as stack unwinding, continues up the chain of nested function calls until a `catch` clause for the exception is found, or the `main` function itself is exited without having found a matching `catch`. During stack unwinding, blocks in the call chain may be exited prematurely. In general, these blocks will have created local objects. Ordinarily, local objects are destroyed when the block in which they are created is exited. Destructors may be invoked during stack unwinding, they should never throw exceptions that the destructor itself does not handle. That is, if a destructor does an operation that might throw, it should wrap that operation in a `try` block and handle it locally to the destructor. The compiler uses the thrown expression to copy initialize a special object known as the exception object. If the expression has a class type, that class must have an accessible destructor and an accessible copy or move constructor. If the expression ahs an array of function type, the expression is converted to its corresponding pointer type. If a `throw` expression dereferences a pointer to base-class type, and that pointer points to a derived-type object, then the thrown object is sliced down; only the base-class part is thrown. ### 18.1.2 Catching an Exception The exception declaration in a catch clause looks like a function parameter list with exactly one parameter. Sometimes a single `catch` cannot completely handle an exception. After some corrective actions, a `catch` may decide that the exception must be handled by a function further up the call chain. A `catch` passes its exception out to another `catch` by rethrowing the exception. A rethrow is a `throw` that is not followed by an expression: ```cpp throw; ``` If a `catch(...)` is used in combination with other `catch` clauses, it must be last. Any `catch` that follows a catch-all can never be matched. ### 18.1.3 Function try Blocks and Constructors To handle an exception from a constructor initializer, we must write the constructor as a function try block. ```cpp template <typename T> Blob<T>::Blob(std::initializer_list<T> il) try : data(std::make_shared<std::vector<T>>(il)) { } catch (const std::bad_alloc &e) { handle_out_of_memory(e); } ``` The only way for a constructor to handle an exception from a constructor initializer is to write the constructor as a function `try` block. ### 18.1.4 The noexcept Exception Specification Under the new standard, a function can specify that it does not throw exceptions by providing a noexcept specification. The keyword `noexcept` following the function parameter list indicates that the function won't throw: ```cpp void recoup(int) noexcept; void alloc(int); ``` If a `noexcept` function does throw, `terminate` is called. The `noexcept` specifier takes an optional argument that must be convertible to `bool`: If the argument is true, then the function won't throw; if the argument is false, then the function might throw: ```cpp void recoup(int) noexcept(true); // recoup won't throw void alloc(int) noexcept(false); // alloc can throw ``` The noexcept operator is a unary operator that returns a bool rvalue constant expression that indicates whether a given expression might throw. ```cpp void f() noexcept(noexcept(g())); ``` If we declare a pointer that has a nonthrowing exception specification, we can use that pointer only to point to similarly qualified functions. ```cpp // both recoup and pf1 promise not to throw void (*pf1)(int) noexcept = recoup; // ok: recoup won't throw; it doesn't matter that pf2 might void (*pft2)(int) = recoup; ``` If a virtual function includes a promise not to throw, the inherited virtuals must also promise not to throw. ## 18.2 Namespaces Libraries that put names into the global namespace are said to cause namespace pollution. Namespaces provide a must more controlled mechanism for preventing name collisions. Namespaces partition the global namespace. A namespace is a scope. ### 18.2.1 Namespace Definitions A namespace definition begins with the keyword `namespace` followed by the namespace name. A namespace scope does not end with a semicolon. Namespace definitions can be discontiguous lets us compose a namespace from separate interface and implementation files. Namespaces that define multiple, unrelated types should use separate files to represent each type that the namespace defines. Template specializations must be defined in the same namespace that contains the original template. Names defined at global scope are defined inside the global namespace. The scope operator can be used to refer to members of the global namespace. Because the global namespace is implicit, it does not have a name; the notation: ```cpp ::member_name ``` Refers to a member of the global namespace. A nested namespace is a namespace defined inside another namespace. ```cpp namespace cplusplus_prime { namespace QueryLib { } } ``` The new standard introduced a new kind of nested namespace, an inline namespace. Unlike ordinary nested namespaces, names in an inline namespace can be used as if they were direct members of the enclosing namespace. An unnamed namespace is the keyword `namespace` followed immediately by a block of declarations delimited by curly braces. Unlike other namespaces, an unnamed namespace is local to a particular file and never spans multiple files. ### 18.2.2 Using Namespace Members A namespace alias can be used to associate a shorter synonym with a namespace name. ```cpp namespace cpluscplus_primer { /* ... */ } namespace primer = cpluscplus_primer; ``` A namespace can have many synonyms or aliases. All the aliases and the original namespace name can be used interchangeably. A using directive, like a `using` declaration, allows us to use the unqualified form of a namespace name. A header that has a `using` directive or declaration at its top-level scope injects names into every file that includes the header. ### 18.2.3 Classes, Namespaces, and Scope The order in which scopes are examined to find a name can be inferred from the qualified name of a function. The qualified name indicates, in reverse order, the scopes that are searched. ### 18.2.4 Overloading and Namespaces ```cpp using NS::print(int); // error: cannot specify a parameter list using Ns::print; // ok: using declarations specify names only ``` ## 18.3 Multiple and Virtual Inheritance Multiple inheritances are the ability to derive a class from more than one direct base class. ### 18.3.1 Multiple Inheritance The derivation list in a derived class can contain more than one base class: ```cpp class Panda : public Bear, public Endangered { /* ... */ }; ``` The order in which base classes are constructed depends on the order in which they appear in the class derivation list. Destructors are always invoked in the reverse order from which the constructors are run. ### 18.3.2 Conversions and Multiple Base Classes Under single inheritance, a pointer or a reference to a derived class can be converted automatically to a pointer or a reference to an accessible base class. As with single inheritance, the static type of the object, pointer, or reference determines which members we can use. ### 18.3.3 Class Scope under Multiple Inheritance Under multiple inheritances, this same lookup happens `simultaneously` among all the direct base classes. ### 18.3.4 Virtual Inheritance By default, a derived object contains a separate subpart corresponding to each class in its derivation chain. If the same base class appears more than once in the derivation, then the derived object will have more than one subobject of that type. This default doesn't work for a class such as `iostream`. An `iostream` object wants to use the same buffer for both reading and writing, and it wants its condition state to reflect both input and output operations. In C++ we solve this kind of problem by using virtual inheritance. Virtual inheritance lets a class specify that it is willing to share its base class. The shared base-class subobject is called a virtual base class. Virtual derivation affects the classes that subsequently derive from a class with a virtual base; it doesn't affect the derived class itself. ```cpp class Raccoon : public vritual ZooAnimal { /* ... */ }; class Bear : virtual public ZooAnimal { /* ... */ }; class Panda : public Bear, public Raccoon, public Endangered {}; ``` Virtual base classes are always constructed prior to nonvirtual base classes regardless of where they appear in the inheritance hierarchy.