B. Stroustrup - The C++ Programming Language (794319), страница 80
Текст из файла (страница 80)
The problem with thatapproach is that you need to remember to ‘‘catch and correct’’ the problem wherever a resource isacquired in an undisciplined way (typically dozens or hundreds of places in a large program),whereas the handler class need be written only once.An object is not considered constructed until its constructor has completed. Then and only thenwill stack unwinding (§13.5.1) call the destructor for the object. An object composed of subobjectsis constructed to the extent that its subobjects have been constructed.
An array is constructed to theextent that its elements have been constructed (and only fully constructed elements are destroyedduring unwinding).A constructor tries to ensure that its object is completely and correctly constructed. When thatcannot be achieved, a well-written constructor restores – as far as possible – the state of the systemto what it was before creation. Ideally, a well-designed constructor always achieves one of thesealternatives and doesn’t leave its object in some ‘‘half-constructed’’ state. This can be simplyachieved by applying the RAII technique to the members.Consider a class X for which a constructor needs to acquire two resources: a file x and a mutex y(§5.3.4). This acquisition might fail and throw an exception.
Class X’s constructor must nevercomplete having acquired the file but not the mutex (or the mutex and not the file, or neither). Furthermore, this should be achieved without imposing a burden of complexity on the programmer.We use objects of two classes, File_ptr and std::unique_lock (§5.3.4), to represent the acquiredresources. The acquisition of a resource is represented by the initialization of the local object thatrepresents the resource:class Locked_file_handle {File_ptr p;unique_lock<mutex> lck;public:X(const char∗ file, mutex& m): p{file,"rw"},// acquire ‘‘file’’lck{m}// acquire ‘‘m’’{}// ...};Now, as in the local object case, the implementation takes care of all of the bookkeeping.
The userdoesn’t have to keep track at all. For example, if an exception occurs after p has been constructedbut before lck has been, then the destructor for p but not for lck will be invoked.This implies that where this simple model for acquisition of resources is adhered to, the authorof the constructor need not write explicit exception-handling code.The most common resource is memory, and string, vector, and the other standard containers useRAII to implicitly manage acquisition and release.
Compared to ad hoc memory managementusing new (and possibly also delete), this saves lots of work and avoids lots of errors.When a pointer to an object, rather than a local object, is needed, consider using the standardlibrary types unique_ptr and shared_ptr (§5.2.1, §34.3) to avoid leaks.358Exception HandlingChapter 1313.3.1 FinallyThe discipline required to represent a resource as an object of a class with a destructor have bothered some. Again and again, people have invented ‘‘finally’’ language constructs for writing arbitrary code to clean up after an exception. Such techniques are generally inferior to RAII becausethey are ad hoc, but if you really want ad hoc, RAII can supply that also.
First, we define a classthat will execute an arbitrary action from its destructor.template<typename F>struct Final_action {Final_action(F f): clean{f} {}˜Final_action() { clean(); }F clean;};The ‘‘finally action’’ is provided as an argument to the constructor.Next, we define a function that conveniently deduces the type of an action:template<class F>Final_action<F> finally(F f){return Final_action<F>(f);}Finally, we can test finally():void test()// handle undiciplined resource acquisition// demonstrate that arbitrary actions are possible{int∗ p = new int{7};// probably should use a unique_ptr (§5.2)int∗ buf = (int∗)malloc(100∗sizeof(int));// C-style allocationauto act1 = finally([&]{delete p;free(buf);// C-style deallocationcout<< "Goodby, Cruel world!\n";});int var = 0;cout << "var = " << var << '\n';// nested block:{var = 1;auto act2 = finally([&]{ cout<< "finally!\n"; var=7; });cout << "var = " << var << '\n';} // act2 is invoked herecout << "var = " << var << '\n';} // act1 is invoked hereSection 13.3.1Finally359This produced:var = 0var = 1finally!var = 7Goodby, Cruel world!In addition, the memory allocated and pointed to by p and buf is appropriately deleted and free()d.It is generally a good idea to place a guard close to the definition of whatever it is guarding.That way, we can at a glance see what is considered a resource (even if ad hoc) and what is to bedone at the end of its scope.
The connection between finally() actions and the resources they manipulate is still ad hoc and implicit compared to the use of RAII for resource handles, but using finally()is far better than scattering cleanup code around in a block.Basically, finally() does for a block what the increment part of a for-statement does for the forstatement (§9.5.2): it specifies the final action at the top of a block where it is easy to be seen andwhere it logically belongs from a specification point of view. It says what is to be done upon exitfrom a scope, saving the programmer from trying to write code at each of the potentially manyplaces from which the thread of control might exit the scope.13.4 Enforcing InvariantsWhen a precondition for a function (§12.4) isn’t met, the function cannot correctly perform its task.Similarly, when a constructor cannot establish its class invariant (§2.4.3.2, §17.2.1), the object isnot usable.
In those cases, I typically throw exceptions. However, there are programs for whichthrowing an exception is not an option (§13.1.5), and there are people with different views of howto deal with the failure of a precondition (and similar conditions):• Just don’t do that: It is the caller’s job to meet preconditions, and if the caller doesn’t dothat, let bad results occur – eventually those errors will be eliminated from the systemthrough improved design, debugging, and testing.• Terminate the program: Violating a precondition is a serious design error, and the programmust not proceed in the presence of such errors.
Hopefully, the total system can recoverfrom the failure of one component (that program) – eventually such failures may be eliminated from the system through improved design, debugging, and testing.Why would anyone choose one of these alternatives? The first approach often relates to the needfor performance: systematically checking preconditions can lead to repeated tests of logicallyunnecessary conditions (for example, if a caller has correctly validated data, millions of tests inthousands of called functions may be logically redundant). The cost in performance can be significant. It may be worthwhile to suffer repeated crashes during testing to gain that performance.Obviously, this assumes that you eventually get all critical precondition violations out of the system.
For some systems, typically systems completely under the control of a single organization,that can be a realistic aim.The second approach tends to be used in systems where complete and timely recovery from aprecondition failure is considered infeasible.
That is, making sure that recovery is completeimposes unacceptable complexity on the system design and implementation. On the other hand,360Exception HandlingChapter 13termination of a program is considered acceptable. For example, it is not unreasonable to considerprogram termination acceptable if it is easy to rerun the program with inputs and parameters thatmake repeated failure unlikely.
Some distributed systems are like this (as long as the program thatterminates is only a part of the complete system), and so are many of the small programs we writefor our own consumption.Realistically, many systems use a mix of exceptions and these two alternative approaches. Allthree share a common view that preconditions should be defined and obeyed; what differs is howenforcement is done and whether recovery is considered feasible. Program structure can be radically different depending on whether (localized) recovery is an aim. In most systems, some exceptions are thrown without real expectation of recovery. For example, I often throw an exception toensure some error logging or to produce a decent error message before terminating or re-initializinga process (e.g., from a catch(...) in main()).A variety of techniques are used to express checks of desired conditions and invariants.
Whenwe want to be neutral about the logical reason for the check, we typically use the word assertion,often abbreviated to an assert. An assertion is simply a logical expression that is assumed to betrue. However, for an assertion to be more than a comment, we need a way of expressing what happens if it is false. Looking at a variety of systems, I see a variety of needs when it comes toexpressing assertions:• We need to choose between compile-time asserts (evaluated by the compiler) and run-timeasserts (evaluated at run time).• For run-time asserts we need a choice of throw, terminate, or ignore.• No code should be generated unless some logical condition is true.
For example, some runtime asserts should not be evaluated unless the logical condition is true. Usually, the logicalcondition is something like a debug flag, a level of checking, or a mask to select amongasserts to enforce.• Asserts should not be verbose or complicated to write (because they can be very common).Not every system has a need for or supports every alternative.The standard offers two simple mechanisms:• In <cassert>, the standard library provides the assert(A) macro, which checks its assertion, A,at run time if and only if the macro NDEBUG (‘‘not debugging’’) is not defined (§12.6.2).
Ifthe assertion fails, the compiler writes out an error message containing the (failed) assertion,the source file name, and the source file line number and terminates the program.• The language provides static_assert(A,message), which unconditionally checks its assertion,A, at compile time (§2.4.3.3). If the assertion fails, the compiler writes out the message andthe compilation fails.Where assert() and static_assert() are insufficient, we could use ordinary code for checking. Forexample:void f(int n)// n should be in [1:max){if (2<debug_level && (n<=0 || max<n)throw Assert_error("range problem");// ...}Section 13.4Enforcing Invariants361However, using such ‘‘ordinary code’’ tends to obscure what is being tested. Are we:• Evaluating the conditions under which we test? (Yes, the 2<debug_level part.)• Evaluating a condition that is expected to be true for some calls and not for others? (No,because we are throwing an exception – unless someone is trying to use exceptions as simply another return mechanism; §13.1.4.2.)• Checking a precondition which should never fail? (Yes, the exception is simply our chosenresponse.)Worse, the precondition testing (or invariant testing) can easily get dispersed in other code and thusbe harder to spot and easier to get wrong.