Essential C++ Chapter 7 learning record (exception handling)

Chapter 7 exception handling

Section 4.6 implementation of triangular_ When the iteratorclass, the iterator has potential errors, which may lead to the generation of error status.

class Triangular
{
    friend class Triangular_iterator;
public:
    static bool is_elem(int);
    static void gen_elements(int length);
    static void gen_elems_to_value(int value);
    static void display(int length,int beg_pos,ostream &os = cout);
private:
	static vector<int> _elems;
    static const int _max_elems = 1024;
};

class Triangular_iterator
{
public:
	Triangular_iterator(int index) :_index(index-1){}
	bool operator==(const Triangular_iterator&) const;
    bool operator!=(const Triangular_iterator&) const;
    int operator*() const;
    Triangular_iterator& operator++();
    Triangular_iterator operator++(int);
    // friend class Triangular;
private:
    void check_integrity() const;
    int _index;
};

Its data members_ index may be set to a value larger than the size of static vector. For users of this class, this problem is not easy to identify.

However, as designers, we can summarize this problem as * * * iterator is no longer valid and can no longer be used by the program*** But we still don't know the harm of this problem to the whole program. Only the users of the iterator know the importance of the problem.

Therefore, our responsibility is to inform the user and tell him what happened. We use the exception handling facility of C + + to complete the notification task.

7.1 throw exception

The exception handling mechanism has two main components: exception identification and sending & exception handling

Generally, whether it is member function or non member function. Can generate and handle exceptions. After the exception occurs, the execution of the normal program is suspended. At the same time, the exception handling mechanism begins to search for places in the program that are capable of handling this exception. After the exception is handled, the execution of the program will resume from the exception handling point.

C + + generates an exception through throw expression:

inline void Triangular_iterator::check_integrity() const
{
    if(_index >= Triangular::_max_elems)
        throw iterator_overflow(_index,Triangular::_max_elems);
    
    if(_index >= Triangular::_elems.size() )
        Triangular::gen_elements(_index + 1);
}

If the first if statement is satisfied, the type is iterator_ The exception object of throw will be thrown, and the second if statement will not be executed.

What is throw ing an exception? An exception is an object. The simplest exception object can be designed as an integer or string:

throw 42;
throw "panic:no buffer!";

Most of the time, the exceptions thrown belong to a specific exception class (perhaps forming an inheritance system). For example, the following definition:

class iterator_overflow{
public:
    iterator_overflow(int index,int max)
                :_index(index),_max(max) {}

    int index() {return _index;}
    int max() {return _max;}

    void what_happened(ostream &os = cerr){
        os<<"Internal error:current index"<<_index
        <<" exceeds maximum bound: "<<_max;
    }

private:
    int _index;
    int _max;
};

So throw in the following code actually calls a constructor with two parameters.

if(_index >= Triangular::_max_elems)
	throw iterator_overflow(_index,Triangular::_max_elems);

We can also specify the name of the thrown object in another way:

if(_index >= Triangular::_max_elems){
	iterator_overflow ex(_index, Triangular::_max_elems);
	throw ex;
}	

7.2 catching exceptions

We can use a single or a series of catch clauses to catch the exception object thrown. The catch clause consists of three parts:

Keyword catch, a type or object in parentheses, a set of statements in braces (used to handle exceptions)

// The extern keyword indicates that this is a declaration, and its definition may be in other files. Note that variables cannot be initialized or functions cannot be defined, 
// Otherwise, it indicates that this is a definition rather than a declaration
extern void log_message(const char*);
extern string err_messages[];
extern ostream log_file;

bool some_function()
{
    bool status = true;
    // ... suppose we get here
    catch(int errno){
        log_message(err_messages[errno]);
        status = false;
    }
    catch(const char* str){
        log_message(str);
        status = false;
    }
    catch(iterator_overflow &iof){
        iof.what_happened(log_file);
        status = false;
    }
    return status;
}

The above catch statements will handle the three exception objects thrown in the previous section respectively

throw 42;
throw "panic:no buffer!";
throw iterator_overflow(_index,Triangular::_max_elems);

This means that the type of exception object will be compared with each catch clause one by one. If the type matches, the contents of the catch clause are executed.

Sometimes we may not be able to complete the complete handling of exceptions. In addition to the recorded information, you also need to re throw the exception to seek the assistance of other catch clauses for further processing:

catch(iterator_overflow &iof)
{
	log_message(ios.what_happened());
	// Re throw the exception and let another catch statement take over
	throw;
}

When re throwing, just write down the keyword throw. It can only appear in the catch clause. It throws the caught exception object again and is handled with the help of another catch clause that matches the type

If we want to catch any type of exception, we can catch it all. Just specify an ellipsis (...) in the exception declaration section, as follows:

// Catch any type of exception
catch(...)
{
	log_message("exception of unknown type");
	// clean up and exit
}

7.3 refining abnormality

The catch clause should appear in combination with the try block. Catch is placed at the end of the try block, which means that if any exception occurs in the try block, it will be handled by the following catch clause.

For example, the following example tries to find a specific elem ent within the range marked by first and last. Iterations within this scope may trigger iterators_ Overflow exception, so put this program code in the try block and specify the iterator to catch in the next catch clause_ Overflow exception.

bool has_elem(Triangular_iterator first,
				Triangular_iterator last, int elem)
{
	bool status = true;
	
	try{
		while(first != last)
		{
            if(*first == elem)
                return status;
            ++first;
		}
	}
    catch(iterator_overflow &iof)
    {
        
        log_message(iof.what_happened()); 
        // There seems to be a mistake in the line above_ What happend() returns is void,
        // But log_message requires const char*
        log_message("check if iterators address same container");   
    }
    status = false;
    return status;
}
// *first calls the overloaded dereference operator
inline int Triangular_iterator::
operator*()
{
    check_integrity();
    return Triangular::_elems[_index];
}

inline void Triangular_iterator::
check_integrity()
{
    if(_index >= Triangular::_max_elems)
        throw iterator_overflow(_index, Triangular::_max_elems);
    // ...
}

Suppose last at a certain moment_ index value greater than_ max_elems, so check_integrity() gets the result true and throws an exception:

The exception handling mechanism starts to check where the exception is thrown and judge whether it is in the try block. If so, check the corresponding catch clause to see whether it has the ability to handle the exception. If so, the exception is handled and the program continues to execute.

However, the throw expression in the above example is not in the try, so the exception handling mechanism does not handle it, which leads to check_ The rest of integrity () will not be executed because the exception handling mechanism terminates the check_integrity(). However, the exception handling mechanism will continue in check_ The caller of integrity () searches for catch clauses with matching types.

So the same question is raised again at the calling end (overloaded dereference operator): check_ The integrity() call operation occurs in the try block? Obviously, there is no, so the dereference operator is interrupted, and the exception handling mechanism continues to trace back to the calling end of the operator (i.e. * first). It is found that at this time, check_integrity() is inside the try block. Therefore, the corresponding catch clause is taken out to view, and the type matching person will be executed, so as to complete the exception handling.

Next, the normal program execution continues with the first line statement below the "executed catch clause", that is:

// An iterator occurred_ When overflow is abnormal or elem is not found, execute the following code
status = false;
return status;

If you keep backtracking until the main() function does not find the appropriate catch() clause, you will call the terminate() provided by the standard library (its default behavior is to interrupt the execution of the whole program)

It can also be seen from the above that what statements are placed in the try block is the subjective intention of the designer. If a statement may throw a field and it is not in the block, the exception will not be caught and processed in this function, which may or may not be a problem. Not every function must handle every possible exception.

For example, the dereference operator above does not set check_ The integrity () call is placed in the try (even if check_integrity() may throw exceptions), because the dereference operator is not ready to handle those exceptions. Even if the exception occurs, it is safe for the dereference operator to be interrupted.

So the question arises: how do you know if a function can safely ignore possible exceptions? Go back to the previous definition of dereference operator:

inline int Triangular_iterator::
operator*()
{
    check_integrity();
    return Triangular::_elems[_index];
}

For comparison, check is assumed_ Integrity () is designed to return true or false (as follows) (instead of the above design that throws exceptions). Our requirement is if_ If the index is invalid, the return statement cannot be executed. Then the above code can be rewritten as:

inline int Triangular_iterator::
operator*()
{
    
    return check_integrity()
    		? Triangular::_elems[_index]
    		: 0;
}

Then the dereference operator above is necessary in check_ Some precautions should be taken when integrity () returns false, and the caller of dereference operator also needs to take corresponding precautions when dereference operator returns 0.

But because of the first check_integrity() shows errors in the way of "throwing exceptions". Therefore, the dereference operator will execute the return statement only when no exceptions are thrown. As long as any exceptions occur, the function will be interrupted "before the return statement is executed". This answers the above question.

The common mistake people make is to confuse C + + exceptions with hardware exceptions such as segmentation fault or bus error. In the face of any thrown C + + exception, you can find a corresponding throw expression somewhere in the program. (some are hidden in the standard library)

7.4 local resource management

The following function first requires the allocation of relevant resources, and then performs some processing operations. These resources are released before the end of the function.

extern Mutex m;
void f()
{
    // Request resources
    int *p = new int;
    m.acquire();

    process(p);
	
    // Release resources
    m.release();
    delete p;
}

The problem is that if process() itself or the function it calls has an exception, the next two statements to release resources will not be executed.

One possible solution is to import the try block and the corresponding catch clause. Catch all exceptions, release all resources, and then throw the exception again:

void f()
{
	try{
		// ...
	}
	catch(...)
	{
        m.release();
        delete p;
        throw;
	}
}

Although this can solve the problem, the code that releases resources must appear twice, and the operations of catching exceptions, releasing resources and re throwing exceptions will further prolong the search time of the exception handler. We hope to write a more protective and automatic processing method. In C + +, this usually means defining an exclusive class.

The so-called resource management means that resource acquisition is initialization in the initialization stage. For objects, the initialization operation takes place in the constructor, and the resource request is also. The release of resources should be completed in destructor. This can not automate resource management, but can simplify our procedures:

#include<memory>
void f()
{
	auto_ptr<int> p(new int);
    MutexLock ml(m);
    process(p);
    // Destructors of p and ml are quietly called here
}

Both p and ml are local objects. If process() is executed correctly, the corresponding destructor will automatically act on p and ml before the end of the function, and the resources will be released; If any exception is thrown during execution, C + + ensures that the destructors of all local objects in the function will be called before the exception handling mechanism terminates a function. Therefore, in the above example, whether an exception is thrown or not, the destructor of p and ml is guaranteed to be called.

The following example implements MutexLock class

class MutexLock{
public:
    MutexLock(Mutex m):_lock(m)
            {_lock.acquire();}
    ~MutexLock(){_lock.release();}
private:
    Mutex &_lock;
};

auto_ptr is the class template provided by the standard library. It will automatically delete the objects allocated through the new expression, such as p in the previous example. Before using it, it overloads the dereference operator and arrow operator (as shown in section 4.6), so that we can use auto like a pointer_ PTR object:

// Use ` Auto_ Before PTR, the corresponding 'memory' header file must be included:
#include <memory>
auto_ptr<string> aps(new string("vermeer"));
string *ps = new string("vermeer");

if((aps->size() == ps->size()) && (*aps == *ps))
// ...

In version 03, the smart pointer STD:: Auto was introduced into the standard library for the first time_ PTR, which was also the only smart pointer type at that time. However, in version 11, it was abandoned.

7.4 standard exceptions

If the new expression cannot allocate enough memory from the free space of the program, it will throw bac_alloc exception object, for example:

vector<string>*
init_text_vector(ifstream &infile)
{
    vector<string> *ptext = 0;
    try{
        ptext = new vector<string>;
        //Open file and file vector
    }
    catch(bad_alloc){
        cerr<<"ouch.head memory exhaausted!\n";
        // Clean and roll out
    }
    return ptext;
}

Statement ptext = new vector < string >; Enough memory will be allocated, and then the vector < string > default constructor will be applied to the heap object, and then the object address will be set to ptext

If there is not enough memory to represent a vector < string > object, default constructor will not be called and ptext will not be set, because bad_ The alloc exception object will be thrown, and the program flow will jump to the catch clause after the try block. The following Declaration:

catch(bad_alloc) // bad_alloc is a class, not an object

No object is declared because we are only interested in the captured type and do not intend to actually manipulate the object in the catch clause

Of course, if you want to inhibit bad_alloc exception is thrown. We can write as follows:

ptext = new(nothrow) vector<string>;
// When the new operation fails, it returns 0 Anyone should verify that ptext is 0 before using it

The standard library defines a set of exception class hierarchy, whose root is an abstract base class called exception. exception

The declaration has a what() virtual function, which will return a const char * to represent the text description of the thrown exception. bad_alloc is derived from the exception base class. The compiler I use will return information such as "bad allocation".

We can also write our own iterator_overflow inherits from the exception base class. First, you must include the standard header file exception, and you must provide your own what():

#include<exception>
class iterator_overflow : public exception{
public:
    iterator_overflow(int index,int max)
                    :_index(index),_max(max) {}
    int index() {return _index;}
    int max() {return _max;}

    // overrides exception::what()
    const char* what() const;
    
private:
    int _index;
    int _max;
}

// iterator_ An implementation of what() of overflow,
// The ostringstream object is used to format the output information
#Include < ssstream > / / the standard header file must be included before using the ostringstream class
#include<string>

const char*
iterator_overflow::what() const
{
    ostringstream ex_msg;
    static string msg;
    // Write the output information to the ostringstream object in memory
    // Converts an integer value to a string representation
    ex_msg << "Internal error:current index "<< _index
        	<<" exceeds maximum bound: " << _max;
    // Extract string object
    msg = ex_msg.str();
    // Extract const char * expression
    return msg.c_str();
}

Code Description:

  • ostringstream class provides "memory output operation", which is output to a string object. When we need to format multiple different types of data into strings, it can automatically_ index,_ When converting numerical objects such as max into corresponding strings, we don't have to consider storage space, conversion algorithm and so on. The member functionstr() provided by ostringstream will return the string object "corresponding to the ostringstream object".
  • The standard library lets what() return a const char * instead of a string object. Conversion function C in stringclass_ Str () converts the string object into a C-style string and returns the const char we want*
  • istringstream class provided by iostream library can convert the string representation (eg: integer value, memory address) of non string data to its actual type

Set iterator_ The advantage of integrating overflow into the standard exception class system is that it can be captured by any program code that "intends to capture the abstract base class exception". This means that the program code can recognize this class without modifying the original program code. We don't have to catch all exceptions in a single catch. For example, the following catch clause can capture all derived classes of exception when bad_ When the alloc exception is thrown, the "bad allocation" information will be printed; When biterator_ When the overflow exception is thrown, it will print "internal error: current index..."

catch(const exception &ex)
{
	cerr<<ex.what()<<endl;
}

Keywords: C++ Back-end

Added by Vern1271 on Wed, 19 Jan 2022 23:13:13 +0200