异常和错误处理

为什么使用异常

https://isocpp.org/wiki/faq/exceptions

让代码更加简洁,清洁,更不容错过错误。但是,使用通常的错误码和判断块,有什么不好的,它们的区别是什么?

使用通常的错误码和判断块,你的错误处理代码和普通的功能函数可能搅和在一起。代码非常的混乱且很难确保处理了所有的错误。

而且有些事情只有在异常处理的帮助下才能执行,比如说构造函数中检测到的错误;此时无法报告错误,只能抛出异常。这是RAII 的基础(Resource Acquisition Is Initialization),RAII 是当前某些最高效的C++ 设计计数的基础。构造函数的工作是为类建立一些基础(创建成员函数执行所需的环境),以及经常需要获取的资源,比如内存,锁,文件,套接字等等。

想象一下,我们没有异常,任何处理构造函数中检测到的错误?注意,通常会调用构造函数来初始化/构造变量中的对象。

    vector<double> v(100000);   // needs to allocate memory
    ofstream os("myfile");      // needs to open a file

vector 或 ofstream 构造函数可能设置变量为一个“被破坏的”状态,因此接下来的每个操作都失败。这不是理想的状态。例如,就stream 来说,如果你忘记了检查打开操作是否成功,减下来的输出可能简单的消失了。对于大多数的类来说,情况可能更糟糕。至少,我们必须像下面这样编写代码:

    vector<double> v(100000);   // needs to allocate memory
    if (v.bad()) { /* handle error */ } // vector doesn't actually have a bad(); it relies on exceptions
    ofstream os("myfile");      // needs to open a file
    if (os.bad())  { /* handle error */ }

对每个对象来说,这都是一个额外的测试。对于包含几个对象的类来说,这很繁琐,尤其是当那些子对象互相依赖。

因此,当没有异常处理,编写构造函数将是非常困难的,对于普通的旧功能来说,我们如何处理呢?我们可以返回一个错误码或者设置一个非本地变量(比如,errno)。不要设置全局变量,需要考虑多线程等很多问题,TLS 中倒是有一个错误码是线程相关的,不用考虑多线程问题。返回值的问题在于,选择错误返回值可能需要一些技巧,而且有些时候可能是不可能的:

    double d = my_sqrt(-1);     // return -1 in case of error
    if (d == -1) { /* handle error */ }
    int x = my_negate(INT_MIN); // Duh?

第一个,所有的非负整数,都是可能的合法值。因此需要选择负数为错误码

第二个,my_negate,即,取反,它的返回值就是所有的值,此时,无法根据返回值判断是否产生异常。一个可能的解决方法是,返回对返回值,其中一个用来表示函数执行的情况(当然,可以通过函数返回值的方式,也可以通过引用参数的方式)。

使用异常处理的常见反对意见:

1. 使用异常处理消耗很大的资源:不对,当代c++ 实现将使用异常的开销减少了百分之几(可能是3%),这是与没有错误处理比较的(错误处理就是常见的检查返回值之类的基本的检查)。写那些处理错误返回值的代码也是有开销的。根据经验,当不抛出异常时,异常处理的代价是很小的。抛出异常时,会产生所有的成本(主要指性能和内存):“普通的代码,不抛出异常的代码“,的执行,比那些使用错误返回值并返回进行测试的代码要高。只有在出现错误的时候才产生消耗(此时消耗比较大)。

2. JSF++ Stroustrup 完成禁止异常处理。JSF++ 是及其实时、安全的应用(飞行控制软件)。如果一个计算执行的时间太长,可能会有人因此死亡。因为这个原因,我们必须确保反应时间,而且不能使用当前的工具的支持级别--使异常做到这一点。这种情况下,甚至动态的内存申请都是被禁止的。实际上,JSF++ 建议错误处理模仿异常的使用,期待,有一天能真正的使用工具来让开发条件放松些--即使用异常处理。

3. 从new i盗用的构造函数中抛出异常,会导致内存泄漏。这是一个由一个编译器中的错误引起的老故事,这个错误已经在10 年前就被立即修复了。

如何使用异常处理

C++ 中,异常是为了知识应用程序有一些无法被本地处理的错误,比如在一个构造函数中获取某个资源的时候,产生了错误。比如:

    class VectorInSpecialMemory {
        int sz;
        int* elem;
    public:
        VectorInSpecialMemory(int s) 
            : sz(s) 
            , elem(AllocateInSpecialMemory(s))
        { 
            if (elem == nullptr)
                throw std::bad_alloc();
        }
        ...
    };

不要像从一个函数中简单的返回一个值一样,简单的就抛出一个异常。

一个关键的计数就是:resource acquisition is initialization(RAII),它使用带有析构函数的类来强加资源管理的顺序。

    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  

当fct 函数中,use f 部分抛出了异常,f 的析构函数依然会被调用,文件也会被正确的关闭,这与常见的不安全用法形成对比。但下面的代码就无法取到这种效果:

    void old_fct(const char* s)
    {
        FILE* f = fopen(s,"r"); // open the file named "s"
        // use f
        fclose(f);  // close the file
    }

当old_fct 中的use f 块抛出异常,或简单的返回了,file 不被关闭。C 程序中,长跳转也是一个额外的冒险。

不应该用异常做什么

C++ 异常是被设计用于支持错误处理的。

1. 仅仅使用throw 来报告一个错误(尤其指出了函数无法做到它宣称的,且无法建立它的后置条件)

2. 使用catch 时,仅仅指定,你知道自己能处理的错误处理动作。(可能通过将其转换为另一种类型并重新抛出该类型的异常,例如捕获bad_alloc 并重新抛出no_space_for_file_buffers

3. 不要使用throw 来指示一个编码错误。使用assert 或其它的机制,以将其发送到调试器或使进程崩溃,并收集crash dump给开发人员分析。

4. 不要使用throw ,如果发现组件中的不变量的意外的违规,使用assert 或其它的方法来终止进程。throw 异常不会修复该内存冲突,而且可能导致重要用户数据的进一步损坏。

还有其它的异常处理的用途,在其它语言中很流行,但C++ 中不是惯用的,并且故意不被C++ 实现很好的支持(这些实现基于,异常用于错误处理,的假设进行了优化)。

不要使用异常来控制程序的执行流程。throw 不仅仅是从函数返回值的替代方法(与return 相似)。这样操作将非常低效,而且将使大部分C++ 程序员产生困惑,他们可能正确的习惯于看到异常仅用于错误处理。同样的,throw 不是从循环中退出的好的方法。

什么情况下try catch throw 可以提高软件质量?

通过消除if 语句的原因之一

将常被替换为 try/catch/throw 的是返回码(或者叫错误码),调用者通过一些条件语句,比如if 来显式判断其值。比如,printf,scanf,malloc 都通过这种方式工作:调用者应该测试返回值,以检查函数是否执行成功执行。

虽然返回码技术有时是最合适的错误处理技术,但此时我们添加一些不必要的if 块有一些讨厌的负面影响:

1. 降低质量:众所周知,条件语句包含错误的可能性大约是任何其它类型语句的十倍。所以,其它的不变,如果你能减少代码中的条件语句或条件块的数量,你就可以写出更加健壮的代码。

2. 减慢上市时间:由于条件语句是与白盒测试所需的测试用例数相关的分支点,因此不必要的条件语句会增加需要用于测试的时间量。基本上,如果你不操作每个分支点,代码中将会有一些指令,这些指令在测试条件下永远不会被看到,直到它们被用户或客户所执行,这样的风险很大。

3. 增加开发消耗:错误发现,错误修复和测试都会因为不必要的控制流复杂性而增加。

因此,与通过返回代码进行错误报告相比,如果使用try/catch/throw 可能会导致代码具有更少的错误,开发成本更低,并且具有更快的上市时间。当然,如果你的组织没有任何try/catch/throw 的经验,您可能希望首先在Demo 项目中使用它,以确保您知道自己在做什么。在将它用到实际的项目前,应该多多练习。

我仍然不相信,一个4 行代码片段显示:返回代码并不比异常更糟糕,为什么我应该在一个数量级更大的应用程序上使用异常?

因为异常规模比返回码更大。

4 行实例代码的问题就是,它只有四行代码。如果是4000 行代码,你就会看到明显的不同。

典型的4行代码的结构是这样的:

try {
  f();
  // ...
} catch (std::exception& e) {
  // ...code that handles the error...
}

同样的例子,使用返回码的方式,编写方式如下:

int rc = f();
if (rc == 0) {
  // ...
} else {
  // ...code that handles the error...
}

问题是:在这样的例子程序中,似乎异常处理没有给编码或者测试或维护成本带来提升;为什么在实际的项目中要使用它们?

理由:异常在实际的开发中真的会帮助你。就算在这里的小例子中,较多的if 也没带来任何的好处。

现实世界中,检测到错误的代码通常传递错误信息到一个能够处理该问题的不同的函数中。这个“错误传递”的过程经常需要穿过一系列的函数:f1 调用f2 调用f3,等等,一个函数可能在f10 甚至f100 的时候被发现了。问题的信息需要被传递通过所有的中间过程,最终回到f1,因为只有f1 有足够的背景信息,以知道该如何处理这个问题。在一个交互app 中,f1 通常靠近主事件循环,但是无论怎样,检测到错误的代码通常不是处理该问题的代码,错误信息需要穿过中间所有的栈帧。

异常处理,处理这种“错误传输”是很简单的:

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();
  // ...
}

仅仅检测错误的f10 函数,以及处理问题的f1 函数稍微有些繁琐。

但是当你使用返回码,错误信息传递的繁琐,强加到了这两个函数中间所有的函数上了。比如下面的实例代码:

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;
}

返回码这种解决方案,将错误逻辑展开了。f2 到 f9 有,与将错误情况传播回f1 相关的,显式手写的代码,这将带来很多问题:

1. 使得f2 到 f9 函数,除了需要的逻辑外,添加了额外的逻辑,这是最常见的导致bug 的原因

2. 增加了代码块

3. 它在函数f2 到 f9 中掩盖了编程逻辑的简单性

4. 它让函数的返回值有了两个不同的功能:“我函数成功了,返回值是xxx”,“我函数失败了,错误信息是yyy”。当xxx和yyy 的数据类型不同,或者它们无法在集合上区分的话,还需要给函数设置额外的参数来表达函数是否执行成功,以及错误执行时的错误信息

如果你只关心内部的f1 到 f10 单个函数的实现,异常处理似乎没有带来太大的提升。但如果将f1 到 f10 整体观察,将看到两种错误处理方式的明显不同。

总结:异常处理的一个好处是:它是将错误信息传播回可以处理错误的调用者的更简洁的方法。另一个好处是,你的函数不需要额外的信息来传递函数“成功”或 “失败”的信息给调用者。

异常处理,如何简化我的函数返回值类型和参数类型?

使用返回代码时,通常需要两个或多个不同的返回值:一个用于指示函数成功并提供计算结果,另一个用于将错误信息传播回调用方。如果有5种方法可能会失败,则可能需要多达6种不同的返回值:“成功计算”返回值,以及5种错误情况中每种情况的可能不同的bit 的package(包?)

将情况转化为两种情况:

1. 我成功了,结果是xxx

2. 我失败了,错误信息是yyy

下面构造了一个Number 类,有加减乘除四则运算。

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;
  // ...
}

但是我们需要考虑,错误处理的情况。加法可能导致溢出,除法可能除零错误,或者向下溢出。我们如何区分上面的“我成功执行,返回值是xxx”和,“我失败了,错误信息是yyy” 

如果我们使用异常,将异常视为单独的返回类型,仅在需要时使用。所以我们只需定义所有的异常并在需要的时候,抛出它们:

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...
  }
}

 但当我们使用返回码来表达这种信息,编码将很繁琐复杂。当你无法将好的计算结果和错误信息区分开,你可能最终需要使用引用参数来表达函数的执行成功与否,以及失败时的错误信息,可以使用引用参数表达是否执行成功,返回值表达正常的计算结果,或者想法,这是细节问题,演示如下:

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;
  }
  // ...
}

问题是:你通常必须将使用返回码的函数的接口弄乱,特别是当有更多的错误信息传播回调用者。比如,如果存在5 中错误条情况,且“错误信息”需要不同的数据结构,则最终可能会出现相当混乱的函数接口。

这种混乱的情况不会在异常中出现,异常可以认为是一种分隔的返回值,仿佛该函数自动根据函数可以抛出的值“增长”新的返回类型和返回值。

注意:当然,有人会想到,使用全局或静态变量或命名空间等等来存储当前函数的返回值,通过这种方法来避免,返回码机制带来的函数参数的混乱以及,传递错误信息带来的混乱。这不是线程安全的,且难以维护,且,需要大量文档,需要各种同步机制或TLS 等等,才能保证可用性,而异常处理,就放在那里,为什么不直接用呢?

异常处理将“好路径” 和 “坏路径”分开,是什么意思?

这里,返回码,之外,异常处理的,另一个好处

好路径,就是,万事大吉,没有问题的时候,的控制流路径

坏路径,问题发生的时候的路径。

异常,当正确设置的时候,可以将好路径和坏路径分开。

void f()  // Using exceptions
{
  try {
    GResult gg = g();
    HResult hh = h();
    IResult ii = i();
    JResult jj = j();
    // ...
  }
  catch (FooError& e) {
    // ...code that handles "foo" errors...
  }
  catch (BarError& e) {
    // ...code that handles "bar" errors...
  }
}

f 正常来说,应该顺序执行g,h,i,j 函数。如果执行过程中产生了任何的“FooError”或"BarError"错误,f 立即处理该错误,并成功返回。如果其他的错误产生了,f 将错误信息传递回调用者。

好路径,坏路径,在上面的代码中很清晰的分开了。try 块中放的是好路径,如果没有错误发生的话,它就像代码展示的那样,一条一条的执行。坏路径是catch 块的主体,和任何调用者中任何匹配的catch 块的主体。

使用返回代码而不是异常使得,很难看到相对简单的算法。好坏路径混杂在一起的时候,代码的控制流很混乱。

int f()  // Using return-codes
{
  int rc;  // "rc" stands for "return code"
  GResult gg = g(rc);
  if (rc == FooError) {
    // ...code that handles "foo" errors...
  } else if (rc == BarError) {
    // ...code that handles "bar" errors...
  } else if (rc != Success) {
    return rc;
  }
  HResult hh = h(rc);
  if (rc == FooError) {
    // ...code that handles "foo" errors...
  } else if (rc == BarError) {
    // ...code that handles "bar" errors...
  } else if (rc != Success) {
    return rc;
  }
  IResult ii = i(rc);
  if (rc == FooError) {
    // ...code that handles "foo" errors...
  } else if (rc == BarError) {
    // ...code that handles "bar" errors...
  } else if (rc != Success) {
    return rc;
  }
  JResult jj = j(rc);
  if (rc == FooError) {
    // ...code that handles "foo" errors...
  } else if (rc == BarError) {
    // ...code that handles "bar" errors...
  } else if (rc != Success) {
    return rc;
  }
  // ...
  return Success;
}

通过将好 / 坏 路径混合,很难看到代码应该做什么。与使用异常处理的版本相比,代码是自解释的,它的基本功能极其明显。

异常处理,使用简单,方便快捷?

No! Wrong! Stop! Go back! Do not collect $200. 英文梗,不知道是个啥,礼貌性的,哈哈哈哈哈哈。

并不是说异常处理简单易行。意思是,异常处理很值得,好处大于付出。

需要付出的:

1. 异常处理不是免费的午餐。它需要纪律以及严谨。下面将介绍它的一些纪律。

2. 异常处理不是万能的。如果整个team 不守纪律,不严谨,无论使用的是错误码还是异常处理,都将产生问题。

3. 异常处理并不是,一招鲜吃遍天。即使整体决定了采用异常处理,并不是所有的地方都使用他。这也是原则的一部分:你应该辨别,什么使用应该通过返回码,什么时候应该通过异常报告错误。

4. Exception handling is a convenient whipping boy. 答题意思,有的人遇见所有的问题,责怪工具,外界环境,干啥都不行。比较理想的情况是,有情有义,乐于学习,遇见问题的时候,想办法解决它。

幸运的是,使用异常处理有足够的智慧和建议已经存在了。异常处理并不是全新的事物:整个行业已经看见百万行代码,以及很多个人的使用异常的努力。异常是可以正确使用的,当正确使用它,就可以改善代码。

异常处理貌似使得工作更加难做;貌似它太难了,肯定不是我自己的问题?

当用不好或者错误的使用一个工具,而没有带来自己想要的结果时,不要责怪工具本身。

使用异常处理的结果如果不是太好,可能是心态问题,下面是一些错误的异常处理心态:

1. 返回码心态:导致程序员用大块的try 块来混乱它们的代码。基本上,它们认为,throw 是一个美化的返回码,try/catch 一个美化的“是否返回码是一个错误”测试,它们将这些try 块放的到处都是,因为,每个函数都可以throw。

2. Java 心态:Java 中,通过显式try /finally 块回收非内存资源。当在c++ 中使用这种思维模式,导致大量的不必要的try 块,与RAII相比,这会让代码混乱并使逻辑更加难以遵循。(这也是我平时使用try 的原因,尝试用RAII 来进行资源回收吧)本质上,代码在好路和坏路之间来回切换(后面的那个指的是,异常中,采用的路径)。使用RAII,代码大部分情况下,更加乐观,所有的都是好路径,清理代码在拥有资源的对象的析构函数中。这也帮助减少了代码review 和 单元测试,因为这种“资源拥有者对象”,可以被隔离验证(使用显式的try /catch 块,每个副本必须单独测试并单独检查,它们不能作为一个组处理)。

3. 围绕,物理的抛出者,而不是,逻辑上的抛出来组织异常类:比如,在一个银行app 中,假设当用户资金不足,五个子函数中的任何一个函数都可能抛出一个异常。这场的做法是抛出一个表示原因的异常,比如:“资金不足”。错误的心态是,为每个子函数抛出一个特定于子系统的异常。例如,foo 子系统可能抛出类FooException 的对象,Bar 子系统,可能抛出类BarException 的对象等。这通常会导致额外的try/catch 块,比如,捕获一个FooException,将其包装成BarException (当Bar 函数中调用了Foo 并捕获了其异常,它再抛出),然后重新抛出。通常,异常类代表的是问题,而不是代表注意到问题的代码块。

4. 使用异常对象中的位/ 数据来区分不同类别的错误:假如一个函数,可以抛出三种各不相关的异常类型:内存错误,文件错误,网络错误,如果你将它们放到同一个异常类中,捕获器有时需要重新判断错误类型。比如,假设catch 只处理内存错误,它捕获到一个组合的异常类,它需要二次判断该异常的类型。通常,首先方法是将错误条件的逻辑类别编码为异常对象的类型,而不是编码到异常对象的数据中。

5. 在子系统的基础上设计异常类:In the bad old days,改革开放前,任何给定返回代码的具体含义对于给定的函数或API 来说都是本地的。因为,一个函数用3 来表示成功,另一个函数可能用3 来表示某个特定的错误。一致性始终是首选,但通常不会发生,因为它不需要发生。有这种心态的人,经常以同样的方式处理C++ 异常:它们假设异常类与子系统相关,是子系统本地化的。此时,代码 中产生了很多额外的try/catch ,以捕获然后抛出相同异常的重新包装变体。在大型系统中,必须以系统范围的心态,设计异常等级。跨越系统子系统边界的异常类,they are part of the intellectual glue that holds the architecture together.它们将架构结合在一起。

6.  使用原始(而不是智能)指针:这实际上是一个非RAII 编码的特例,但很常见。如上所述,使用原始指针的结果是许多额外的try/catch 块,其唯一目的是删除对象,然后重新抛出异常。

7. 将逻辑错误和运行时状态搞混:例如,假设您有一个函数f(Foo * p),绝不能使用nullptr调用它。 但是你发现有人在某处有时会传递nullptr。 有两种可能性:要么是传递nullptr,因为它们从外部用户那里得到了错误的数据(例如,用户忘记填写字段并最终导致nullptr),或者他们只是在他们自己的代码中犯了错误。 在前一种情况下,您应该抛出异常,因为它是运行时情况(即,通过仔细的代码审查无法检测到的东西;它不是错误)。 在后一种情况下,您应该明确修复调用者代码中的错误。 你仍然可以添加一些代码来在日志文件中再次发送消息,如果它再次发生,你甚至可以抛出异常,但你不能仅仅改变f(Foo * p)中的代码。 你必须,必须,必须修复f(Foo * p)的调用者的代码。

当代码中有很多的try 块,如何处理?

即使你使用的是try/catch/throw 语法,也可能有返回代码的思维方式,比如,几乎可以在每个调用周边放置一个try块:

void myCode()
{
  try {
    foo();
  }
  catch (FooException& e) {
    // ...
  }
  try {
    bar();
  }
  catch (BarException& e) {
    // ...
  }
  try {
    baz();
  }
  catch (BazException& e) {
    // ...
  }
}

虽然用了try/catch/throw 语法,但整体结构还是和返回码相似,同时,软件开发/测试/维护成本和使用返回码几乎相同。话句话说,这种方法对于使用返回代码并没有太大的帮助。一般来说,这是不好的形式。

一种方法是:为每个try 问自己一个问题:是什么在这里使用try 块。可能的答案有:

1. 这样我就可能实际的处理这个函数。我的catch 子句处理错误并继续执行而不抛出任何其他异常。我的调用者,永远不知道发生了异常,我的catch 子句不抛出任何异常,并且它不返回任何错误代码。此时,你保持try 的代码,它应该是有用的。

2. 我的catch, blah blah blah,然后重新抛出异常。此时,考虑将try 块转变为一个对象,该对象的析构函数做这些blah blah blah。例如,如果你有一个try 块,它的catch 语句关闭一个文件,然后重新抛出异常,考虑重新编写代码,使用一个文件对象,该对象的析构函数关闭该文件。这就是通常所说的RAII。

3. 将异常重新包装为另一个异常类,此时,考虑重新设计异常的等级,以免去这一层异常捕获。

等等...

如果你,返回码的思维,中毒太深,且在使用异常处理的时候,很受影响,参考如下页面:https://isocpp.org/wiki/faq/how-to-learn-cpp#mentoring

我是否可以从构造函数、析构函数中抛出一个异常

1. 构造,可以:无论何时,当无法正确初始化一个对象,可以抛出一个异常。使用throw 退出构造函数没有真正令人满意的替代办法。

2. 析构,不建议:可以在析构函数中抛出异常,但异常最好不要跑出了析构的边界。如果析构函数通过发出异常退出,则可能发生各种不良的事情,因为标准库的基本规则和语言本身自己的规则被违反,不要这样做。

更详细的解释:

http://stroustrup.com/3rd_safe0.html

构造函数失败的时候,应该怎么办呢?

抛出异常

构造函数没有返回类型,返回错误码是不可能的,只能抛出异常,暴露问题。如果你的项目中,抛出异常是不可取的,“最不好”的解决办法是,通过设置一个内部状态位,将对象置于“僵尸”状态, 这样,即使它在技术上仍然存在,该对象的的行为有点像死了。

僵尸状态有很多不好的方面。你需要添加一个query(检查员)成员函数,以检查这个僵尸,位,此时,你的类的用户可以查看它的对象是否是真的活的。每个构造该对象的地方都应该伴随一个判断,每个成员函数也应该添加一个判断。

实际编码中,僵尸对象的处理将使得代码非常的丑。还是尽量使用异常,僵尸对象是不得已的选项。

注意,如果构造函数通过抛出异常退出了,与对象绑定的内存被自动释放了,不会产生内存泄漏的问题。

这个主题有一些细节,具体来说,如果构造函数本身分配内存,你需要知道如何防止内存泄漏,另外,如果你使用“placement ”new 而不是上面实例代码中使用的普通new ,你还需要知道会发生什么。

析构函数失败的时候,应该做什么

写信息到日志文件,终止进程,或打电话给aunt tilda?(哦)。但不要抛出异常。

这里是为什么:c++ 规则是,当析构,是在一个异常的“栈展开”过程中被调用的,不要在此抛出异常。比如,如果有人抛出Foo(),栈会被展开,所有的

throw Foo()

}

catch(Foo e)

{

之间的栈帧都会被pop。这叫做栈展开。

栈展开期间,那些栈帧中存在的所有局部变量被析构。如果其中的某个析构抛出一个异常(比如,抛出Bar object),c++ 运行时系统处于一种“不赢”的局面,它是否应该忽略Bar 并最终进入:

 }
  catch (Foo e)
  {

这是它原来的目的地,或者,去寻找:

 }
  catch (Bar e)
  {

无论如何选择都将丢失一些信息。

C++ 此时将调用terminate,退出进程。

析构,永远都不要抛出异常,或者说,当处理另外一个异常的时候,不要从析构函数中抛出一个异常,但,需要判断当前是否在展开(当前我都不知道怎么判断),并编写处理两种情形的代码(当前处于展开中,当前不处于展开中)。为了避免这种复杂的情形,这里一个简单的原则就是,析构里面不要抛出异常,不然,问题将很难定位。当然,存在1%的情况,自己考虑去吧。

当我的构造函数可能抛出异常,我该如何处理资源?

对象中的每个成员都应该清理它自己。

比如,将申请的内存放到一个智能指针成员对象,当智能指针消息,智能指针对应的对象的内存块也会被释放。模板:std::unique_ptr 是一个智能指针的示例。

顺便一提,如果你认为自己的类将被使用智能指针申请,对用户友好些,提供一个写好的typedef :

#include <memory>
class Fred {
public:
  typedef std::unique_ptr<Fred> Ptr;
  // ...
};

该typedef 简化了使用对象的所有代码的语法。

当有人抛出异常了,我该如何修改一个字符数组的字符长度,以避免内存泄漏?

如果你需要进行一些字符串操作,不要使用数组,使用一些类字符换的类。

比如:

void userCode(const char* s1, const char* s2)
{
  char* copy = new char[strlen(s1) + 1];    // make a copy
  strcpy(copy, s1);                         //   of s1...
  // use a try block to prevent memory leaks if we get an exception
  // note: we need the try block because we used a "dumb" char* above
  try {
    // ...code that fiddles with copy...
    char* copy2 = new char[strlen(copy) + strlen(s2) + 1];  // append s2
    strcpy(copy2, copy);                                    //   onto the
    strcpy(copy2 + strlen(copy), s2);                       //     end of
    delete[] copy;                                          //       copy...
    copy = copy2;
    // ...code that fiddles with copy again...
  }
  catch (...) {
    delete[] copy;   // we got an exception; prevent a memory leak
    throw;           // re-throw the current exception
  }
  delete[] copy;     // we did not get an exception; prevent a memory leak
}

总体: s1 + s2然后 做一些操作。

使用char* 很无聊,且容易出错。为什么不用字符串 类呢?编译器可能提供了一些字符串 类,使用更方面,也更安全。比如,std::string:

#include <string>           // Let the compiler see std::string
void userCode(const std::string& s1, const std::string& s2)
{
  std::string copy = s1;    // make a copy of s1
  // ...code that fiddles with copy...
  copy += s2;               // append s2 onto the end of copy
  // ...code that fiddles with copy again...
}

string 类是自动内存管理的,比自己反复的操作内存方便很多,也更安全,它们供的自动管理功能有:

1. 当字符串边长,重新申请内存

2. 函数结尾,自动析构

3. 捕获并重新抛出所有类型的异常

我该抛出什么?

ANYTHING

通常,最好抛出对象,不是内置的对象。如果可能的话,应该抛出从std::exception 类继承的类的实例。这样,用户会更加开心。它们捕获到std::exception 错误会无所适从。

最常见的做法是抛出一个临时的:

#include <stdexcept>
class MyException : public std::runtime_error {
public:
  MyException() : std::runtime_error("MyException") { }
};
void f()
{
   // ...
   throw MyException();
}

我该catch 什么?

为了保持C ++传统的“有多种方法可以做到这一点”(翻译:“给程序员选择和权衡,以便他们可以决定什么对他们来说最适合他们的情况”),C ++允许你有多种选择:

1. 值

2. 引用

3. 指针

实际上,具有声明函数参数的所有灵活性,特定的catch 子句与特定异常是否匹配(即,将被捕获)的规则与调用函数时参数兼容性的规则几乎完全相同。

catch 的原则是:除非有很好的理由,尽量通过引用来catch。避免通过值catch,值会产生拷贝,拷贝的附加行为随着值的类型不同而不同。只有在非常特殊的情况下才能通过指针捕获。

MFC 似乎鼓励,catch-by-pointer,我是否应该同样这样做?

看情况,如果你在使用MFC 并catch 他们的异常,无论如何,按照他们的方式去做。这同样适用于任何框架,在框架中,就按照框架的一些约定做。

当你创建自己的框架,且并不直接依赖于MFC,不要像MFC 一样,catch by pointer。像MFC一样的库,早于C++ 语言中异常处理的标准化,其中一些库使用向后兼容的异常处理形式,需要(至少鼓励)通过指针捕获。

通过指针捕获的问题是,不清楚谁(如果有人)负责删除指向对象。例如:

MyException x;
void f()
{
  MyException y;
  try {
    switch ((rand() >> 8) % 3) {  // the ">> 8" (typically) improves the period of the lowest 2 bits
      case 0: throw new MyException;
      case 1: throw &x;
      case 2: throw &y;
    }
  }
  catch (MyException* p) {
    // should we delete p here or not???!?
  }
}

有三个基本的问题:

1. catch 块中,决定是否删除指针比较困难。比如,如果对象x 对于catch 子句的范围是不可访问的,例如,当它隐藏在某个类的私有部分中或者在某个其它编译单元中是静态的时,可能很难弄清楚要做什么。

2. 如果你通过在throw 中永远使用new 来避免了第一个问题,将带来另一个问题:如果异常是由系统内存量少导致的,这里的new 使用的堆内存可能失败。

3. 如果你通过在throw 中永远不使用new 解决问题(catch 中永远不使用delete),你可能无法在本地申请异常对象(他们可能过早的被释放了),此时要担心线程安全的问题,锁、信号量等。(静态对象本质上不是线程安全的)

如果使用引用,代码实现起来会简单很多。

避免抛出指针表达式,避免通过指针捕获,除非你使用一个想要你这么做的已经存在的库。

throw; ,在throw 关键字后没有异常对象,是什么意思,在哪里用?

class MyException {
public:
  // ...
  void addInfo(const std::string& info);
  // ...
};
void f()
{
  try {
    // ...
  }
  catch (MyException& e) {
    e.addInfo("f() failed");
    throw;
  }
}

这里是,将当前的异常,重新抛出。通过在程序的重要函数中添加适当的catch 子句,可以使用此习惯用法实现一种简单形式的堆栈跟踪。

另一个re-throwing 用法,是异常分发:

void handleException()
{
  try {
    throw;
  }
  catch (MyException& e) {
    // ...code to handle MyException...
  }
  catch (YourException& e) {
    // ...code to handle YourException...
  }
}
void f()
{
  try {
    // ...something that might throw...
  }
  catch (...) {
    handleException();
  }
}

这个用法,允许单个函数(handleException)用来在一系列其它的函数中处理异常。

如何多态的throw?

有些人喜欢这样编码:

class MyExceptionBase { };
class MyExceptionDerived : public MyExceptionBase { };
void f(MyExceptionBase& e)
{
  // ...
  throw e;
}
void g()
{
  MyExceptionDerived e;
  try {
    f(e);
  }
  catch (MyExceptionDerived& e) {
    // ...code to handle MyExceptionDerived...
  }
  catch (...) {
    // ...code to handle other exceptions...
  }
}

发现,抛出的异常,跑到了catch(...)。因为抛出的时候,没有多态的抛出。函数f 中,throw e 语句,使用表达式e 的静态类型的相同类型来抛出了一个对象。换句话说,它抛出了MyExceptionBase的一个实例。throw 语句,表现的好像被抛出的对象是被拷贝的,而不是制作“虚拟副本”

class MyExceptionBase {
public:
  virtual void raise();
};
void MyExceptionBase::raise()
{ throw *this; }
class MyExceptionDerived : public MyExceptionBase {
public:
  virtual void raise();
};
void MyExceptionDerived::raise()
{ throw *this; }
void f(MyExceptionBase& e)
{
  // ...
  e.raise();
}
void g()
{
  MyExceptionDerived e;
  try {
    f(e);
  }
  catch (MyExceptionDerived& e) {
    // ...code to handle MyExceptionDerived...
  }
  catch (...) {
    // ...code to handle other exceptions...
  }
}

这样可正确的修正这一行为。

当我抛出这个对象,它会被复制多少次?

看情况,可能是0次。

被抛出的对象,必须有一个共有访问属性的,拷贝构造函数。编译器被允许生成拷贝被抛出对象任意次数的代码,包括0次。然而,即使编译器从来不真正的拷贝被抛出的对象,它必须确保,异常类的拷贝构造函数存在且,可访问。

为什么c++ 不提供finally 结构?

因为C++ 支持一种几乎更好的替代方案:RAII 技术。基本思想是:通过本地变量来表示一个自愿,本地对象的析构函数将释放资源。这样,开发人员不会忘了释放资源:

 // wrap a raw C file handle and put the resource acquisition and release
    // in the C++ type's constructor and destructor, respectively
    class File_handle {
        FILE* p;
    public:
        File_handle(const char* n, const char* a)
            { p = fopen(n,a); if (p==0) throw Open_error(errno); }
        File_handle(FILE* pp)
            { p = pp; if (p==0) throw Open_error(errno); }
        ~File_handle() { fclose(p); }
        operator FILE*() { return p; }   // if desired
        // ...
    };
    // use File_handle: uses vastly outnumber the above code
    void f(const char* fn)
    {
        File_handle f(fn,"rw"); // open fn for reading and writing
        // use file through f
    } // automatically destroy f here, calls fclose automatically with no extra effort
      // (even if there's an exception, so this is exception-safe by construction)

在一个系统中,在最坏的情况下,我们需要为每个资源提供一个“资源句柄”类。 但是,我们不必为每次获取资源都有一个“finally”子句。 在现实系统中,资源获取比资源种类多得多,因此“资源获取是初始化”技术导致的代码少于使用“最终”构造。

为什么在捕获了一个异常后,我不能恢复执行?

换句话说,为什么C ++不能提供一个原语来返回抛出异常并从那里继续执行的点?

基本上,从异常处理程序恢复的人,永远不能确定抛出throw之后的代码,是为了处理执行,只是继续好像什么也没发生过。异常处理程序在恢复之前无法知道“正确”的上下文。为了获得正确的代码,throw的作者和catch的编写者需要对彼此的代码和上下文有深入的了解。这会产生复杂的相互依赖性,无论何时允许它都会导致严重的维护问题。

Stroustrup认真考虑了在设计C ++异常处理机制时允许恢复的可能性,并且在标准化过程中对此问题进行了相当详细的讨论。请参阅C ++的设计和演变的异常处理章节。

如果要在抛出异常之前检查是否可以解决问题,请调用一个仅在问题无法在本地处理时检查然后抛出的函数。

猜你喜欢

转载自blog.csdn.net/qq_18218335/article/details/84405156