Skip to main content

Error Handling and Exceptions

 

Error Handling and Exceptions

- The worst possible way to deal with any problem is for the program to produce the wrong answer without informing the user - a silent failure. 

- When the program deals with an error by aborting, it should do so with the explicit intention of the programmer. (i.e. the programmer should call assert or check a condition then call abort) And the program should give an error message to the user. Simply segmentation fault due to an invalid memory access is never appropriate.

- Sometimes we would prefer to handle the error more gracefully. Graceful error handling requires making the user aware of the problem, as well as allowing the program to continue to function normally.

- For example, we would present the user with some options to remedy the situation, like entering a different input, select a different file, retry a failed operation, etc. 

- As our programming skills develop, we should get into the practice of writing bullet proof code - code that can gracefully handle any problem that can happen.

- In C, functions return a value indicating the error, which the caller must check.

- However, C++ provides a better approach(called exceptions). 


C-Style Error Handling

- fclose, printf can fail

- But for all the errors, we only know error happens without knowing the reason why error happens exactly.

- One solution may be to set errno to an error code indicating what went wrong. The errno.h not only defines errno but also the various standard error codes, such as ENOMEM (insufficient memory), ENOENT (The file was not found), and about a hundred others. 

- Most C library functions will set errno appropriately when they fail. Therefore, if the failure stems from these functions, we don not need to set errno ourselves. 

- However, since errno is global ,we must be careful of other library calls that might change its value if we plan to use it. 

- There are a handful of functions that work with errno, such as perror (which prints a description of the error the current value of errno represents), and strerror (which returns a char * pointing at such a string).

C++-style: Exception

- We want to remove the possibility that an error can be silently ignored.

- We want error handling to be as unobtrusive as possible in our code - reducing the "cluttered" feeling given by C-style error handling.

- We want our error handling mechanism to convey extra information about the error. In particular, we would like to be able to convey arbitrary information (any kind we want). Besides the type of the problem, we might want more specific and detailed information of the problem.

- To do that, C++ introduces exceptions, which involve two concepts:
    1) throw
    2) try/catch
 
- A programmer places code where an error might occur in a try block. The try block is immediately followed by one or more catch blocks. Each catch block specifies how to handle a particular type of exception.

- Code that detects an error it cannot handle throws an exception to indicate the problem.

- This exception can be any C++ type, including an object type. The function that has detected the problem can include any additional information it wants to communicate to the error handling code by including it in the object throws.

- Once an exception is thrown, it propagates up the call stack, forcing each function to return (such that each of their stack frames is destroyed) until a suitable exception handler is found, i.e. until it caught with a try/catch block.

- Each time a stack frame is destroyed, destructors are invoked as usual to clean up objects. However, if one of these destructors throws an exception that will propagates out of the destructor, the program crashes.

- Therefore. we should design our own destructor.

- Code that can handle an error describes how to deal with that error with try/catch blocks. Code that cannot handle an error can simply do nothing and the error will propagate to the caller - it cannot be ignored. 

Executing Code with Exceptions

- Throwing an exception is accomplished by executing a throw statement (throw e is an expression, but its type is void, Therefore, it is only used as a statement by itself). The keyword throw, followed by an expression that evaluates to the exception to throw.
e.g.
throw std::exception();

- This expression constructs an unnamed temporary (of type std::exception via its default constructor) then throws the resulting object.

- The exception then propagates up the call stack, forcing functions to return and destroying their frames, until it finds a suitable exception handler in the form of a try block with a following catch block that can catch an exception of a compatible type.

- In C++, we can throw any type of object, but should only throw subtypes of std::exception. 

- To use std::exception, we should #include <stdexcept>

- Once an exception is thrown, control is transferred to the nearest suitable exception handler, possibly unwinding the stack (forcing the functions to return, destroying their frames, and executing the destructors for objects in those frames). 

- When code might throw an exception and the program knows how to handle the situation, the programmer writes the code within a try block. Then we writes one or more handlers, each of which specifies a type of exception to catch.
e.g.
try {
    ...
}
catch (std::exception & e) {
    ...
}

- When an exception is thrown, control transfers into the exception handler (catch block) to allow the program to deal with the situation.

- More specifically, when an exception is thrown, the following steps occur:
    1) The exception object is potentially copied out of the frame into some location that will persist through handling. The potentially qualifier appears here as the compiler may eliminate the copy as long as it does not change the behavior of the program. The compiler may arrange for the unnamed temporary to be directly allocated into some other part of memory. 
    2) If the execution arrow is currently inside a try block, then it jumps to the close curly brace of the try block. And then beginning to match the exception type against the handler types in the order they appear. If the execution arrow encounters a catch block capable of catching the exception type, then it is thought caught, and the process continues in step 4.
    3) If the execution arrow was not inside a try block in step 2, then the exception propagates out of the function it is in. It destroying the function's frame, including any objects inside it. In the process of destroying the frame, the destructors for the objects are invoked as if the function had returned normally. The execution arrow then returns to wherever the function was called and repeat step 2.
    4) Once the error is caught, it is combined with the variable name declared in the () of the catch block. Then the code in the catch block is executed according to the normal rules with one exception. This exception is that if a statement of the form throw; is encountered inside the catch block, then the exception being handled is re-thrown. 
    5) If the execution arrow reaches the close curly brace of the handler, then the program is finished handling the exception. Then the exception object is deallocated, and the execution continues normally at the statement following the close curly brace.

- For the catch block, one can specify that it will catch any type. But in doing so, it cannot bind the exception to a variable. We can do so by writing: 
try {
    //code here
}
catch (...) {
    //code here
}

- In this example, we  catch an exception of an unknown type, we cannot bind it to any variable.


- For a try block, there is a variation called a function try block: It is primarily of use in a constructor, where the programmer wants to catch exceptions that may occur in the initializer list. 

- In a function try block, the keyword try appears before the function body and before the initializer list if there is. 

- The handler appears after the close curly brace of the function. e.g.
class X {
    Y aY;
    Z aZ;
public:
    X() try : aY(42), aZ(0) {
        ...
    }
    catch(std::exception & e) {
        ...
    }
};

- If an exception occurs in the initializer list or the body of the constructor, it will be caught by the handler after the function. 

- However, a function try block on a constructor has a special behavior: since the object cannot be properly initialized, the exception is automatically re-thrown. (like putting throw; in the last line of the function try block) Anything that was successfully constructed is destroyed before entering the handler.

- For a normal function, a function try block covers the entire body of the function and may return as normal. If the function returns a value, then the function try block should return a value. 

Exception as Part of a Function's Interface

- Function may include an exception specification - a list of the types of exception it may throw. Such a declaration is added to a function by writing throw() with the exception types listed in the parentheses.
e.g.
int f(int x) throw(std::bad_alloc, std::invalid_argument);
int g(int * p, int n) throw();
int h(int z);

- The exception specification is part of the interface of the function. It tells the code that uses the function what types of error conditions might result from the use of that function. 

- Code that uses functions with exception specifications knows to either handle those errors or declare them in its own exception specification. 

- When overriding a method that provides an exception specification, the overriding method must have an exception specification that is the same or more restrictive than the exception specification of the inherited method.
i.e.
4 options:
//option 1: same as the parent class
int f(int x) throw(std::bad_alloc, std::invalid_argument);
//option 2: more restrictive
int f(int x) throw(std::bad_alloc);
//option 3: more restrictive
int f(int x) throw(std::invalid_argument);
//option 4: more restrictive, cannot throw any exception
int f(int x) throw();

- The reason for this restriction arises from polymorphism. Child class's method cannot throw exceptions parent class's method did not allow. If so, it is not a suitable substitute for the parent class.

- Unfortunately, C++'s exception specifications were deprecated in C++1. The reason for that is: the exception specification is not checked by the compiler. Instead, the compiler must generate code that enforces the guarantees at runtime.

- Violating the exception specification is generally handled in a very heavyweight fashion: the program is killed.

Exception Corner Cases

- C++ has two special functions: unexpected and terminate to handle corner cases.

- unexpected() is called when a function throws exception that is not allowed by its exception specification.The default behavior of unexpected() depends on whether the exception specification allows std::bad_exception. If so, then the unexpected() function throws a std::bad_exception and exception handling continues normally. If it is not permitted, then unexpected() calls terminate().

- As name suggests, the default behavior of the terminate() function is to terminate the program by calling abort().

- This function is called in the following situation:
    1) an exception that propagates outside of main
    2) an exception that propagates out of a destructor during stack unwinding
    3)  throw; when no exception is being handled
    4) By the default implementation of unexpected()

- Programmer may supply his/her own behavior if desired by calling set_unexpected or set_terminate, passing in a function pointer to specify the new behavior of calls to unexpected() terminate() respectively.

- Another set of corner cases arises when an exception is thrown during object construction. When such a situation occurs, the piece of the object that are already initialized are destroyed, and the memory for the object is freed. 

- If an array of objects are allocated with new[], and If the Nth object's constructor throws an exception , then the objects from indices N-1 down to 0 will be destructed in reverse order of their construction.

Using Exception Properly

- Exceptions are for error conditions. 

- Guideline of how and when to use exceptions:
    1) Exception are only for error conditions. 
    2) Throw an unnamed temporary, created locally
        i.e. throw exn_type(args)
    3) Re-throw an exception only with throw;
        i.e. In exception handler, if we must re-throw an exception, we should do throw;
    4) Catch by reference
        i.e. We should catch by catch(const exn_type_name & e)
    5) Declare handlers from most to least specific
        i.e. The catch block for the same try block should be declared in order from most specific type to least specific type.
 i.e.
We should write like the following:
try {
    ...
}
catch (child_exn_type & e) {
    ...
}
catch (parent_exn_type & e) {
    ...
}

    6) Destructors should never throw exceptions.
        i.e. If destructors perform any operation that could throw, we must find a way to handle it appropriately with try/catch.
    7) Exception types should inherit from std::exception.
        i.e. If we write our own exception class, it should inherit from std::exception or one of its subclasses. 
    8) Keep exception types simple.
        i.e. If we write our exception class, it should be quite simple. It should not have any behavior that can throw an exception. Typically, we want the exception class to have no dynamic allocation at all (new can throw std::bad_alloc)
    9) Override the what method in your own exception types.
        i.e. The std::exception class declares the method: 
virtual const char * what () const throw();
        This method provides a description of the exception that happened. It returns a C-style string (just const char *, not a std::string) as those are simpler.
    10) Be aware of the exception behavior of all code you work with.

Exception Safely

- Exception safely is important.

- If exception happens, previous variable should be unchanged.

- A stronger exception guarantee is the no-throw guarantee, which promises that the code will never throw an exception - if any of the operations it performs fail, it will handle the resulting exceptions. 

- Destructors should always provide a no-throw guarantee. If not, then any code that creates instances of that class cannot provide guarantee, as it cannot ensure that the object it created will be properly destroyed. 

- There are better approaches than try/catch.

Resource Acquisition Is Initialization

- Design principle: Resource Acquisition is Initialization: object in the local frame that is constructed when the resource is allocated and whose destructor frees that resource. (RAII)

- C++ STL provides a templated class, std::auto_ptr<T>, which is designed to help write exception safe code with the RAII paradigm. 

- The basic use of the std::auto_ptr<T> template is to initialize it by passing the result of new to its constructor.  Then making use of the get to obtain the underlying pointer or the overloaded * operator to dereference that pointer.
e.g.
X * someFunction(A & anA, B & aB) {
    std::auto_ptr<X> myX(new X());
    std::auto_ptr<Y> myY(new Y());
    aB.someMethod(myX.get());
    *myY = anA.somethingElse();
    return myX.release();
}

- Here, if the allocation fails, the exception destroys auto_ptr, freeing the memory we allocated. 

- *myY = *myY.get().

- If either of these methods throws an exception, both auto_ptrs will be destroyed, freeing the corresponding memory. 

- The release will make auto_ptr owns no pointers.

- The return value of release is the pointer that was owned by the auto_ptr. When myX is destroyed, it will do nothing

- The return value of release is the pointer that was owned by the auto_ptr.

- std::auto_ptr does not work with arrays as it uses delete and not delete[].

- The Boost Library provides a boost::interprocess:unique_ptr<T, D> templated class. The second template argument specifies how to delete the owned pointer, making it possible to use it properly with arrays.

- C++11 adapts this template into std::unique_ptr and deprecates std::auto_ptr.

Exception Safely Idiom: Temp-and-Swap

- swap function
class someClass {
    void makeSomeModification() {
        someClass temp(*this);
        ...
        std::swap(*this, temp);
    }
};

- std::swap is a templated function that performs the swap operation using the copy constructor and copy assignment operator of the class involved. 

- In C++11, it instead uses move constructor and move assignment operator. 

- Accordingly, we cannot use the general std::swap to implement the assignment operator. 

Real C++

- An experienced C++ programmer seldom has pointers to dynamically allocated objects, as they violate this principle. 

- Instead, they use RAII to manage their objects. 

summary& understanding  after reading <<All of Programming>> Chapter 19

Comments

Popular posts from this blog

Templates

  Template - Polymorphism is the ability of the same code to operate on different types. This ability to operate on multiple types reduces code duplication by allowing the same piece of code to be reused across the different types it can operate on. - Polymorphism comes in a variety of forms. What we are interested in at the moment is parametric polymorphism, meaning that we can write our code so that it is parameterized over what type it operates on.  -That is, we want to declare a type parameter T and replace int with T in the above code. -Then, when we want to call the function, we can specify the type for T and get the function we desire. C++ provides parametric polymorphism through templates. Templated Functions - We can write a templated function by using the keyword template followed by the template parameters in angle brackets (<>). - Unlike function parameters, template parameters may be types, which are specified with typename where the type of the parameter wo...

前端 优化代码体积

当我使用npm run build的时候,项目构建了很久。所以考虑用create-react-app网站下面的工具来缩小代码体积  Analyzing the Bundle Size https://create-react-app.dev/docs/analyzing-the-bundle-size 更改完成后 npm run build npm run analyze 可以看到以下的图片: 其中main.js有1.57mb 然后起服务 serve -s build -l 8000 进入到首页之后,打开network,查看js,发现main.js有500kb。这个500kb是已经用gzip压缩过了,但是却还有这么大。500*3=1500说明源文件有1.5mb左右 其中, antd占了25%, recharts占了13%, react-dom占了7.6%,dnd-kit占了2.8% 其中recharts用于统计页面,dnd-kit用于拖拽排序-编辑器页面。 所以在加载首页的时候,先不加载编辑页面和统计页面的js的话,体积会小很多。 路由懒加载 因为项目中,体积占比最大的是Edit和Stat页面(编辑和统计页面),所以考虑使用路由懒加载,拆分bundle,优化首页体积 router文件中,之前: import Edit from "../pages/question/Edit" ; import Stat from "../pages/question/Stat" ; 现在: const Edit = lazy (() => import ( "../pages/question/Edit" )); const Stat = lazy (() => import ( "../pages/question/Stat" )); 为了让生成的文件更加可读,可以改成下面这样: const Edit = lazy ( () => import ( /* webpackChunkName: "editPage" */ "../pages/question/Edit" ) ); const Stat ...

useMemo的使用场景

 useMemo是用来缓存 计算属性 的。 计算属性是函数的返回值,或者说那些以返回一个值为目标的函数。 有些函数会需要我们手动去点击,有些函数是直接在渲染的时候就执行,在DOM区域被当作属性值一样去使用。后者被称为计算属性。 e.g. const Component = () => { const [ params1 , setParams1 ] = useState ( 0 ); const [ params2 , setParams2 ] = useState ( 0 ); //这种是需要我们手动去调用的函数 const handleFun1 = () => { console . log ( "我需要手动调用,你不点击我不执行" ); setParams1 (( val ) => val + 1 ); }; //这种被称为计算属性,不需要手动调用,在渲染阶段就会执行的。 const computedFun2 = () => { console . log ( "我又执行计算了" ); return params2 ; }; return ( < div onClick = { handleFun1 } > //每次重新渲染的时候我就会执行 computed: { computedFun2 () } </ div > ); }; 上面的代码中,在每次点击div的时候,因为setParams1的缘故,导致params1改变,整个组件都需要重新渲染,也导致comptedFunc2()也需要重新计算。如果computedFunc2的计算量很大,这时候重新计算会比较浪费。 可以使用useMemo: const Com = () => { const [ params1 , setParams1 ] = useState ( 0 ); const [ params2 , setParams2 ] = useState ( 0 ); //这种是需要我们手动去调用的函数 const handleFun1 ...