《C++ Primer第五版》读书笔记(16)--Tools for Large Programs

来源:互联网 发布:sgs报告数据解读 编辑:程序博客网 时间:2024/05/01 18:35

Among the needs that distinguish large-scale applications are:
•The ability to handle errors across independently developed subsystems
•The ability to use libraries developed more or less independently
•The ability to model more complicated application concepts


18.1 Exception Handling
Exception handling allows independently developed parts of a program to communicate about and handle problems that arise at run time. Exceptions let us separate problem detection from problem resolution.


18.1.1 Throwing an Exception
In C++, an exception is raised by throwing an expression. When a throw is executed, the statement(s) following the throw are not executed. Instead, control is transferred from the throw to the matching catch. The fact that control passes from one location to another has two important implications:
•Functions along the call chain may be prematurely exited.
•When a handler is entered, objects created along the call chain will have been destroyed.
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. If no matching catch is found, the program calls the library terminate function. As its name implies, terminate stops execution of the program.


Objects Are Automatically Destroyed during Stack Unwinding
When a block is exited during stack unwinding, the compiler guarantees that objects created in that block are properly destroyed.
If an exception occurs in a constructor, then the object under construction might be only partially constructed. Even if the object is only partially constructed, we are guaranteed that the constructed members will be properly destroyed.

Destructors and Exceptions
During stack unwinding, destructors are run on local objects of class type.Because destructors are run automatically, they should not throw. If, during stack unwinding, a destructor throws an exception that it does not also catch, the program will be terminated.

The Exception Object
The compiler uses the thrown expression to copy initialize (§ 13.1.1, p. 497) a special object known as the exception object. As a result, the expression in a throw must have a complete type.

18.1.2 Catching an Exception
The exception declaration in a catch clause looks like a function parameter list with exactly one parameter. 
As with function parameters, if the catch parameter has a nonreference type, then the parameter in the catch is a copy of the exception object; changes made to the parameter inside the catch are made to a local copy, not to the exception object itself. If the parameter has a reference type, then like any reference parameter, the catch parameter is just another name for the exception object. Changes made to the parameter are made to the exception object.
Ordinarily,a catch that takes an exception of a type related by inheritance ought to define its parameter as a reference.


Finding a Matching Handler
Multiple catch clauses with types related by inheritance must be ordered from most derived type to least derived


Rethrow
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:
throw;
In general, a catch might change the contents of its parameter. If, after changing its parameter, the catch rethrows the exception, then those changes will be propagated only if the catch’s exception declaration is a reference


The Catch-All Handler
To catch all exceptions, we use an ellipsis for the exception declaration. Such handlers, sometimes known as catch-all handlers, have the form catch(...).


18.1.3 Function try Blocks and Constructors
A catch inside the constructor body can’t handle an exception thrown by a constructor initializer because a try block inside the constructor body would not yet be in effect when the exception is thrown.
To handle an exception from a constructor initializer, we must write the constructor as a function try block.
template<typename T> Blob<T>::Blob(std::initializer_list<T> il) try : data(std::make_shared<std::vector<T>>(il)) {
/* empty body*/
} catch(const std::bad_alloc &e) { handle_out_of_memory(e); }


Notice that the keyword try appears before the colon that begins the constructor initializer list and before the curly brace that forms the (in this case empty) constructor function body. The catch associated with this try can be used to handle exceptions thrown either from within the member initialization list or from within the constructor body.
It is worth noting that an exception can happen while initializing the constructor’s parameters. Such exceptions are notpart of the function tryblock. The function try block handles only exceptions that occur once the constructor begins executing. As with any other function call, if an exception occurs during parameter initialization, that exception is part of the calling expression and is handled in the caller’s context.

18.1.4 The noexcept Exception Specification
It can be helpful both to users and to the compiler to know that a function will not throw any exceptions. Knowing that a function will not throw simplifies the task of writing code that calls that function. Moreover, if the compiler knows that no exceptions will be thrown, it can (sometimes) perform optimizations that must be suppressed if code might throw.
Under the new standard, a function can specify that it does not throw exceptions by providing a noexcept specification.
void recoup(int) noexcept;  // won't throw
void alloc(int);  // might throw


Violating the Exception Specification

The compiler in general cannot, and does not, verify exception specifications at compile time: (原来编译还有很多干不了的事情)
// this function will compile, even though it clearly violates its exception specification
void f() noexcept  // promises not to throw any exception
{
throw exception();  // violates the exception specification
}
Earlier versions of C++ had a more elaborate scheme of exception specifications that allowed us to specify the types of exceptions that a function might throw. A function can specify the keyword throw followed by a parenthesized list of types that the function might throw. The throw specifier appeared in the same place as the noexceptspecifier does in the current language.
This approach was never widely used and has been deprecated in the current standard. Although these more elaborate specifiers have been deprecated, there is one use of the old scheme that is in widespread use. A function that is designated by throw()promises not to throw any exceptions:
void recoup(int) noexcept;  // recoup doesn't throw
void recoup(int) throw();  // equivalent declaration
These declarations of recoup are equivalent. Both say that recoup won’t throw.

Arguments to the noexcept Specification
void recoup(int) noexcept(true);  //  recoup won't throw
void alloc(int) noexcept(false);  //  alloc can throw

The noexcept Operator

The noexcept operator is a unary operator that returns a bool rvalue constant expression that indicates whether a given expression might throw.
For example, this expression yields true:
noexcept(recoup(i))// true if calling recoup can't throw, false otherwise
More generally,
noexcept(e)
is true if all the functions called by e have nonthrowing specifications and e itself does not contain a throw. Otherwise, noexcept(e) returns false.
void f() noexcept(noexcept(g())); // f has same exception specifier as g
noexcept has two meanings: It is an exception specifier when it follows a function’s parameter list, and it is an operator that is often used as the bool argument to a noexceptexception specifier.


Exception Specifications and Pointers, Virtuals, and Copy Control
If we declare a pointer that has a nonthrowing exception specification, we can use that pointer only to point to similarly qualified functions. A pointer that specifies (explicitly or implicitly) that it might throw can point to any function, even if that function includes a promise not to throw:
// 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 (*pf2)(int) = recoup;
pf1 = alloc; // error: alloc might throw but pf1 said it wouldn't
pf2 = alloc; // ok: both pf2 and alloc might throw
If a virtual function includes a promise not to throw, the inherited virtuals must also promise not to throw. On the other hand, if the base allows exceptions, it is okay for the derived functions to be more restrictive and promise not to throw.
If all the corresponding operation for all the members and base classes promise not to throw, then the synthesized member is noexcept. If any function invoked by the synthesized member can throw, then the synthesized member is noexcept(false).

18.1.5 Exception Class Hierarchies

The only operations that the exceptiontypes define are the copy constructor, copy-assignment operator, a virtual destructor, and a virtual member named what.
The what function returns a const char*that points to a null-terminated character array, and is guaranteed not to throw any exceptions.

18.2 Namespaces
When an application uses libraries from many different vendors, it is almost inevitable that some of these names will clash. Libraries that put names into the global namespace are said to cause namespace pollution.
Namespaces provide a much more controlled mechanism for preventing name collisions. Namespaces partition the global namespace. A namespace is a scope. By defining a library’s names inside a namespace, library authors (and users) can avoid the limitations inherent in global names
18.2.1 Namespace Definitions
namespace cplusplus_primer {
class Sales_data { / * ... * /};
Sales_data operator+(const Sales_data&,const Sales_data&);
class Query { /* ... */ };
class Query_base { /* ... */};
} // like blocks, namespaces do not end with a semicolon

Namespaces Can Be Discontiguous
Writing a namespace definition:
namespace nsp {
// declarations
}
either defines a new namespace named nspor adds to an existing one.
Namespaces that define multiple, unrelated types should use separate files to represent each type (or each collection of related types) that the namespace defines.

Defining the Primer Namespace

// ----Sales_data.h----// #includes should appear beforeopening the namespace

#include<string>
namespace cplusplus_primer {
                class Sales_data { /* ...*/};
                Sales_data operator+(constSales_data&,const Sales_data&);
                // declarations for theremaining functions in the Sales_data interface
}

// ----Sales_data.cc----// be sure any #includes appear before opening the namespace
#include "Sales_data.h"
namespace cplusplus_primer {
                // definitions forSales_data members and overloaded operators
}


Defining Namespace Members
Assuming the appropriate declarations are in scope, code inside a namespace may use the short form for names defined in the same (or in an enclosing) namespace. Althougha namespace member can be defined outside its namespace, such definitions must appear in an enclosing namespace. We cannot define this operator in an unrelated namespace.


Template Specializations
Template specializations must be defined in the same namespace that contains the original template.
As with any other namespace name, so long as we have declared the specialization inside the namespace, we can define it outside the namespace:
// we must declare the specialization as a member of std
namespace std {
template <> struct hash<Sales_data>;
}
// having added the declaration for the specialization to std
// we can define the specialization outside the std namespace
template <> struct std::hash<Sales_data>
{
size_t operator()(const Sales_data& s) const
{ return hash<string>()(s.bookNo) ^hash<unsigned>()(s.units_sold) ^hash<double>()(s.revenue); }
// other members as before
};


The Global Namespace
Names defined at global scope (i.e., names declared outside any class, function, or namespace) are defined inside the global namespace. The global namespace is implicitly declared and exists in every program.
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
::member_name


Nested Namespaces
namespace cplusplus_primer {
// first nested namespace: defines the Query portion of the library
namespace QueryLib {
class Query { /* ... */ };
Query operator&(const Query&, const Query&);
// ...
}
// second nested namespace: defines the Sales_data portion of the library
namespace Bookstore {
class Quote { /* ... */ };
class Disc_quote : public Quote { /* ... */ };
// ...
}
}
Nested namespace names follow the normal rules: Names declared in an inner namespace hide declarations of the same name in an outer namespace. Names defined inside a nested namespace are local to that inner namespace. Code in the outer parts of the enclosing namespace may refer to a name in a nested namespace only through its qualified name.


Inline Namespaces
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. That is, we need not qualify names from an inline namespace by their namespace name. We can access them using only the name of the enclosing namespace.
inline namespace FifthEd {
// namespace for the code from the Primer Fifth Edition
}
namespace FifthEd { // implicitly inline
class Query_base { /* ... * /};
// other Query-related declarations
}
The keyword must appear on the first definition of the namespace. If the namespace is later reopened, the keyword inline need not be, but may be, repeated.
Inline namespaces are often used when code changes from one release of an application to the next. For example, we can put all the code from the current edition of the Primer into an inline namespace. Code for previous versions would be in noninlined namespaces.


Unnamed Namespaces
An unnamed namespace is the keyword namespace followed immediately by a block of declarations delimited by curly braces. Variables defined in an unnamed namespace have static lifetime: They are created before their first use and destroyed when the program ends.
An unnamed namespace may be discontiguous within a given file but does not span files. Each file has its own unnamed namespace. If two files contain unnamed namespaces, those namespaces are unrelated.
Prior to the introduction of namespaces, programs declared names as static to make them local to a file. The use of file statics is inherited from C. In C, a global entity declared static is invisible outside the file in which it is declared. The use of file static declarations is deprecated by the C++ standard. File statics should be avoided and unnamed namespaces used instead.


18.2.2 Using Namespace Members
namespace alias:

namespace primer = cplusplus_primer;
A namespace can have many synonyms, or aliases. All the aliases and the original namespace name can be used interchangeably.

Using Declarations: A Recap
A using declaration introduces only one namespace member at a time. It allows us to be very specific regarding which names are used in our programs.

Using Directives
Unlike a using declaration, we retain no control over which names are made visible—they all are when when use the using directive.


Headers and using Declarations or Directives
A header that has a using directive or declaration at its top-level scope injects names into every file that includes the header. Ordinarily, headers should define only the names that are part of its interface, not names used in its own implementation. As a result, header files should not contain using directives or using declarations except inside functions or namespaces.

18.2.3 Classes, Namespaces, and Scope
18.2.4 Overloading and Namespaces

Overloading and using Declarations
The functions introduced by a using declaration overload any other declarations of the functions with the same name already present in the scope where the using declaration appears. If the usingdeclaration appears in a local scope, these names hide existing declarations for that name in the outer scope. If the using declaration introduces a function in a scope that already has a function of the same name with the same parameter list, then the usingdeclaration is in error. Otherwise, the using declaration defines additional overloaded instances of the given name. The effect is to increase the set of candidate functions.
Overloading and using Directives
A using directive lifts the namespace members into the enclosing scope. If a namespace function has the same name as a function declared in the scope at which the namespace is placed, then the namespace member is added to the overload set:
Overloading across Multiple using Directives
If many using directives are present, then the names from each namespace become part of the candidate set.


18.3 Multiple and Virtual Inheritance
18.3.1 Multiple Inheritance
Multiply Derived Classes Inherit State from Each Base Class.
Derived Constructors Initialize All Base Classes
Inherited Constructors and Multiple Inheritance

Under the new standard, a derived class can inherit its constructors from one or more of its base classes.It is an error to inherit the same constructor (i.e., one with the same parameter list) from more than one base class:
struct Base1 {
Base1() = default;
Base1(const std::string&);
Base1(std::shared_ptr<int>);
};
struct Base2 {
Base2() = default;
Base2(const std::string&);
Base2(int);
};
// error: D1 attempts to inherit D1::D1 (const string&) from both base classes
struct D1: public Base1, public Base2 {
using Base1::Base1;  // inherit constructors from Base1
using Base2::Base2;  // inherit constructors from Base2
};
A class that inherits the same constructor from more than one base class must define its own version of that constructor:
struct D2: public Base1, public Base2 {
using Base1::Base1;  // inherit constructors from Base1
using Base2::Base2;  // inherit constructors from Base2
// D2 must define its own constructor that takes a string
D2(const string &s): Base1(s), Base2(s) { }
D2() = default; // needed once D2 defines its own constructor
};


18.3.2 Conversions and Multiple Base Classes
The compiler makes no attempt to distinguish between base classes in terms of a derived-class conversion. Converting to each base class is equally good.


18.3.3 Class Scope under Multiple Inheritance
Under single inheritance, the scope of a derived class is nested within the scope of its direct and indirect base classes (§ 15.6, p. 617). Lookup happens by searching up the inheritance hierarchy until the given name is found. Names defined in a derived class hide uses of that name inside a base.
When a class has multiple base classes, it is possible for that derived class to inherit a member with the same name from two or more of its base classes. Unqualified uses of that name are ambiguous.

18.3.4 Virtual Inheritance
Virtual derivation affects the classes that subsequently derive from a class with a virtual base; it doesn’t affect the derived class itself
Using a Virtual Base Class
// the order of the keywords public and virtual is not significant

class Raccoon : public virtual ZooAnimal { /* ... */ };
class Bear : virtual public ZooAnimal { /* ... */ };
The virtual specifier states a willingness to share a single instance of the named base class within a subsequently derived class. There are no special constraints on a class used as a virtual base class.
We do nothing special to inherit from a class that has a virtual base:
class Panda : public Bear,public Raccoon, public Endangered {
};


18.3.5 Constructors and Virtual Inheritance
In a virtual derivation, the virtual base is initialized by the most derived constructor. In our example, when we create a Panda object, the Panda constructor alone controls how the ZooAnimal base class is initialized.
Even though ZooAnimal is not a direct base of Panda, the Panda constructor initializes ZooAnimal:
Panda::Panda(std::stringname, bool onExhibit): ZooAnimal(name, onExhibit, "Panda"),
Bear(name, onExhibit),Raccoon(name, onExhibit),Endangered(Endangered::critical),
sleeping flag(false)  { }

How a Virtually Inherited Object Is Constructed
The construction order for an object with a virtual base is slightly modified from the normal order: The virtual base subparts of the object are initialized first, using initializers provided in the constructor for the most derived class. Once the virtual base subparts of the object are constructed, the direct base subparts are constructed in the order in which they appear in the derivation list.









0 0
原创粉丝点击