C++异常使用思考

1.概述

我觉得C++异常的讨论应该从三个方面去展开:

  • 异常的设计意义层面;
  • 什么样的异常应该用异常机制处理;
  • 异常的底层实现和效率层面;

由于只是玩具级程序员,暂时更关注1、2,第3点后面再研究

不过暂时实在还是太过于才疏学浅,以后再回来看看有多少地方是理解错的吧。

2. 异常的设计意义

C++有两种常用的异常处理思路:

  • 错误码机制,在函数返回值中包含式地返回错误码,结合与errno类似的全局错误码;
  • 异常机制,throw、try、catch;

按照isocpp网站的FAQ,异常设计出来是解决以下问题的:

  • 解决构造函数的异常问题,因为构造函数根本就没有返回值;
  • 将代码的正常流和异常流分隔开,增加可读性;
  • 穿透性,发生错误不必显式地层层返回,例如f1()–>f2()–>f3()在f3()才调用fopen(),不使用异常的话就只能层层返回,写过不少类似代码,确实非常麻烦。而使用异常的话,在f3()中throw,然后在f1()中catch就可以;

对应例子在附录中列出。

3.什么样的异常应该用异常机制处理

依照个人理解对异常进行一些分类

  • 内核和硬件产生的异常,可以从硬件来划分,硬件有cpu、内存、io:
    • cpu,除以0异常;
    • 内存,segmentation fault,好像内核会记录进程能访问的内存地址,访问了没记录的地址或者内核地址,就会segmentation fault。这样就很好理解为什么野指针、越界、字符串漏了’\0’经常会segmentation fault了。具体参考[关于segmentation fault的贴子](Segmentation Fault 是如何被检测到的? - theanarkh的回答 - 知乎 https://www.zhihu.com/question/338188059/answer/775038605);
    • 各种io应该也可能会出错???
  • 用户态的软件异常:
    • 利用封装过之后的 数据库连接、malloc()、new、fopen()、socket()之类申请资源的函数,然后申请失败;
    • 用户输入、配置文件(其中的ip等内容)等外部输入;
    • 业务逻辑异常;
    • 造成越界、野指针等等的代码编写类异常;
  • 按照内部和外部来分类:
    • 外部输入、资源申请等造成的错误;
    • 内部逻辑造成的错误;
  • 按照处理方式来分类:
    • 可忽略异常;
    • 再次尝试异常,修改 / 等待后;
    • 终止式异常;

我认为这些只有外部输入、资源等等的异常是最应该抛异常的;内部逻辑方面可以参考是否能增强可读性,等等因素考虑是否应该抛异常;而可能的越界等等的判断,应该用assert()。

4.相关

知乎相关讨论

ISOCPP网站相关说明

附录.网站中所列的例子

  • 构造函数例子:

    /*
    In C++, exceptions are used to signal errors that cannot be handled locally, such as the failure to acquire a resource in a constructor. For example:
    */
        class VectorInSpecialMemory {
            int sz;
            int* elem;
        public:
            VectorInSpecialMemory(int s) 
                : sz(s) 
                , elem(AllocateInSpecialMemory(s))
            { 
                if (elem == nullptr)
                    throw std::bad_alloc();
            }
            ...
        };
    
    /*
    Do not use exceptions as simply another way to return a value from a function. Most users assume – as the language definition encourages them to – that ** exception-handling code is error-handling code **, and implementations are optimized to reflect that assumption.
    
    A key technique is resource acquisition is initialization (sometimes abbreviated to RAII), which uses classes with destructors to impose order on resource management. For example:
    */
    
        void fct(string s)
        {
            File_handle f(s,"r");   // File_handle's constructor opens the file called "s"
            // use f
        } // here File_handle's destructor closes the file  
    
    If the “use f” part of fct() throws an exception, the destructor is still invoked and the file is properly closed. This contrasts to the common unsafe usage:
    
        void old_fct(const char* s)
        {
            FILE* f = fopen(s,"r"); // open the file named "s"
            // use f
            fclose(f);  // close the file
        }
    /*
    If the “use f” part of old_fct throws an exception – or simply does a return – the file isn’t closed. In C programs, longjmp() is an additional hazard.
    */
    
    • 正常流和异常流分离例子:

      /*
      Let’s work a simple example: we would like to create a Number class that supports the four arithmetic operations: add, subtract, multiply and divide. This is an obvious place for overloaded operators, so let’s define them:
      */
      class Number {
      public:
        friend Number operator+ (const Number& x, const Number& y);
        friend Number operator- (const Number& x, const Number& y);
        friend Number operator* (const Number& x, const Number& y);
        friend Number operator/ (const Number& x, const Number& y);
        // ...
      };
      
      void f(Number x, Number y)
      {
        // ...
        Number sum  = x + y;
        Number diff = x - y;
        Number prod = x * y;
        Number quot = x / y;
        // ...
      }
      
      /*
      But then we have a problem: handling errors. Adding numbers could cause overflow, dividing could cause divide-by-zero or underflow, etc. Whoops. How can we report both the “I succeeded and the result is xxx” as well as “I failed and the error information is yyy”?
      
      If we use exceptions, it’s easy. Think of exceptions as a separate return type that gets used only when needed. So we just define all the exceptions and throw them when needed:
      */
      void f(Number x, Number y)
      {
        try {
          // ...
          Number sum  = x + y;
          Number diff = x - y;
          Number prod = x * y;
          Number quot = x / y;
          // ...
        }
        catch (Number::Overflow& exception) {
          // ...code that handles overflow...
        }
        catch (Number::Underflow& exception) {
          // ...code that handles underflow...
        }
        catch (Number::DivideByZero& exception) {
          // ...code that handles divide-by-zero...
        }
      }
      /*
      But if we use return codes instead of exceptions, life gets hard and messy. When you can’t shove both the “good” number and the error information (including details about what went wrong) inside the Number object, you will probably end up using extra by-reference parameters to handle one of the two cases: either “I succeeded” or “I failed” or both. Without loss of generality, I will handle the computed result via a normal return value and the “I failed” case via a by-reference parameter, but you can just as easily do the opposite. Here’s the result:
      */
      class Number {
      public:
        enum ReturnCode {
          Success,
          Overflow,
          Underflow,
          DivideByZero
        };
      
        Number add(const Number& y, ReturnCode& rc) const;
        Number sub(const Number& y, ReturnCode& rc) const;
        Number mul(const Number& y, ReturnCode& rc) const;
        Number div(const Number& y, ReturnCode& rc) const;
        // ...
      };
      
      int f(Number x, Number y)
      {
        // ...
      
        Number::ReturnCode rc;
        Number sum = x.add(y, rc);
        if (rc == Number::Overflow) {
          // ...code that handles overflow...
          return -1;
        } else if (rc == Number::Underflow) {
          // ...code that handles underflow...
          return -1;
        } else if (rc == Number::DivideByZero) {
          // ...code that handles divide-by-zero...
          return -1;
        }
      
        Number diff = x.sub(y, rc);
        if (rc == Number::Overflow) {
          // ...code that handles overflow...
          return -1;
        } else if (rc == Number::Underflow) {
          // ...code that handles underflow...
          return -1;
        } else if (rc == Number::DivideByZero) {
          // ...code that handles divide-by-zero...
          return -1;
        }
      
        Number prod = x.mul(y, rc);
        if (rc == Number::Overflow) {
          // ...code that handles overflow...
          return -1;
        } else if (rc == Number::Underflow) {
          // ...code that handles underflow...
          return -1;
        } else if (rc == Number::DivideByZero) {
          // ...code that handles divide-by-zero...
          return -1;
        }
      
        Number quot = x.div(y, rc);
        if (rc == Number::Overflow) {
          // ...code that handles overflow...
          return -1;
        } else if (rc == Number::Underflow) {
          // ...code that handles underflow...
          return -1;
        } else if (rc == Number::DivideByZero) {
          // ...code that handles divide-by-zero...
          return -1;
        }
      
        // ...
      }
      
      
      • 穿透的例子:

        //Exceptions make it easy to do this “error propagation”:
        
        void f1()
        {
          try {
            // ...
            f2();
            // ...
          } catch (some_exception& e) {
            // ...code that handles the error...
          }
        }
        
        void f2() { ...; f3(); ...; }
        void f3() { ...; f4(); ...; }
        void f4() { ...; f5(); ...; }
        void f5() { ...; f6(); ...; }
        void f6() { ...; f7(); ...; }
        void f7() { ...; f8(); ...; }
        void f8() { ...; f9(); ...; }
        void f9() { ...; f10(); ...; }
        
        void f10()
        {
          // ...
          if ( /*...some error condition...*/ )
            throw some_exception();
          // ...
        }
        /*
        Only the code that detects the error, f10(), and the code that handles the error, f1(), have any clutter.
        
        However using return-codes forces “error propagation clutter” into all the functions in between those two. Here is the equivalent code that uses return codes:
        */
        int f1()
        {
          // ...
          int rc = f2();
          if (rc == 0) {
            // ...
          } else {
            // ...code that handles the error...
          }
        }
        
        int f2()
        {
          // ...
          int rc = f3();
          if (rc != 0)
            return rc;
          // ...
          return 0;
        }
        
        int f3()
        {
          // ...
          int rc = f4();
          if (rc != 0)
            return rc;
          // ...
          return 0;
        }
        
        int f4()
        {
          // ...
          int rc = f5();
          if (rc != 0)
            return rc;
          // ...
          return 0;
        }
        
        int f5()
        {
          // ...
          int rc = f6();
          if (rc != 0)
            return rc;
          // ...
          return 0;
        }
        
        int f6()
        {
          // ...
          int rc = f7();
          if (rc != 0)
            return rc;
          // ...
          return 0;
        }
        
        int f7()
        {
          // ...
          int rc = f8();
          if (rc != 0)
            return rc;
          // ...
          return 0;
        }
        
        int f8()
        {
          // ...
          int rc = f9();
          if (rc != 0)
            return rc;
          // ...
          return 0;
        }
        
        int f9()
        {
          // ...
          int rc = f10();
          if (rc != 0)
            return rc;
          // ...
          return 0;
        }
        
        int f10()
        {
          // ...
          if (...some error condition...)
            return some_nonzero_error_code;
          // ...
          return 0;
        }
        
        
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值