25

我一直在读到我不应该抛出一个std::string或其他一些分配内存的类。喜欢这里或更重要是第 3 点。 -不要嵌入std::string对象

所以现在我试图在我的项目中插入boost::exception,我看到了什么:很多字符串

为什么 boost 不遵守自己的建议?

如果我有不能硬编码的参数,比如安全的配置文件,我怎么能把它们放到异常中,而不使用std::string

还是指南不只使用std::string一个尽可能少使用std::string的指南?我有点困惑……

我做了一些研究。如果我错了,请纠正我。


如果我理解正确,这完全是关于抛出期间的分配以及分配的内存发生了什么。因此,如果我在构造函数中分配内存并且无法在异常的析构函数中释放内存,则会丢失内存,这将产生内存泄漏。但是在抛出之前分配这个是可以的,所以异常是干净的。

我试过这个:

struct xexception {
  int *ttt[10];
  xexception() {
    ttt[0] = new int[0xfffffffL];
    ttt[1] = new int[0xfffffffL];
    ttt[2] = new int[0xfffffffL];
    ttt[3] = new int[0xfffffffL];
    ttt[4] = new int[0xfffffffL];
    ttt[5] = new int[0xfffffffL];
    ttt[6] = new int[0xfffffffL];
    ttt[7] = new int[0xfffffffL];
    ttt[8] = new int[0xfffffffL];
    ttt[9] = new int[0xfffffffL];
  }

  ~xexception() throw() {
    //never happen
    delete[] ttt[0];
    delete[] ttt[1];
    delete[] ttt[2];
    delete[] ttt[3];
    delete[] ttt[4];
    delete[] ttt[5];
    delete[] ttt[6];
    delete[] ttt[7];
    delete[] ttt[8];
    delete[] ttt[9];
  }
};

int main(int argc, const char *argv[]) {
  try {
    throw(xexception());
  }
  catch (const xexception &e) {
    std::cerr << "\nttt " << e.ttt[0][0] << std::endl;
  }
  catch (std::bad_alloc) {
    std::cerr << "bad alloc" << std::endl;
  }

  return 0;
}

结果是,我得到了bad_alloc和巨大的内存泄漏。

现在,如果我之前进行分配,它也会抛出 bad_alloc 但在创建异常之前。


我对异常概念的例外是:

谁在乎?如果我的程序中有一个 bad_alloc,由于 memory_leak 或其他原因(我说的是 PC 上的程序而不是微控制器),我还有其他问题。也许我可以弄清楚发生了 bad_alloc,但是在哪里呢?在我的 alloc 函数期间(可能是 1000 个)或在std::string(我知道它是字符串但......不可能操纵字符串的内存......或者它消散)。

try {
  // where is the error???
  int *x = new int[100];  // here?
  ....
  int *y = new int[100];  // or here?
  ....
  int *z = new int[100];
  ....
  int *w = new int[100];
  ....
  int *t = new int[100];
  ....
  int *f = new int[100];

  ....

  std::string str("asdfasdfasdfasdfasdfasdfasdf"); // maybe here
}
catch (the error) {
  ....
}

进而?我应该试着弄清楚它发生在哪里吗?因此我会使用valgrind而不是例外。

void foo() {
  int *i = new int[1];
  foo();
}

try {
  foo();
}
chatch( bad_boy ) {
  go_exception_handler_go(parameters); // oh, shit happens: also an stack_overflow may happend, cause stack is also full
}

或者我应该操纵错误消息并记录它,最终会抛出下一个bad_alloc。

请不要误解我的意思。自从我看到了 boost::exception 我已经重写了我的异常类(直到等待答案),但我也认为没有必要捡起每一粒沙子。

4

2 回答 2

18

该建议基本上是在告诉您“不要使用任何可能在异常中引发异常的构造”。这是因为如果您在尝试抛出异常时遇到异常,C++ 运行时将立即调用terminate()并终止您的程序。

现在,如果(任何一个)所涉及的异常terminate()无论如何都会调用(这是未捕获异常的默认设置),那么您真的不需要担心它。例如,如果您的应用程序无法处理bad_alloc(无法从内存不足中恢复),那么您无需担心std::string可能会抛出它的复制构造函数(例如 )。

但是,如果您希望能够捕获并从中恢复bad_alloc,则需要确保您的异常复制构造函数都不会导致异常。如果您正在编写其他应用程序将使用的库,则不应假设该应用程序不想处理bad_alloc.

C++11 通过尽可能使用移动构造函数(而不是复制构造函数)使这变得更容易。由于std::string永远不会抛出异常的移动构造函数std:string,只要正确实现移动构造函数并确保使用它们,您就可以安全地在异常类型中使用 a 。请注意,在 throw 表达式中要抛出的对象的初始构造不是异常抛出过程的一部分,因此构造函数可以抛出异常而不会导致双重异常(和 terminate())。因此,如果您有:

throw some_function();

some_function可能会抛出异常(例如bad_alloc)而不返回要抛出的对象,这很好。如果它没有抛出异常(并返回一个有效对象),则异常类型的移动构造函数将用于异常抛出过程(如果可用),并且该移动构造函数不得抛出异常。


完全独立于上述情况,每当您调用时,new您需要确保delete在每种可能的情况下都只有一个位置会调用,否则您将泄漏内存(或因双重删除而崩溃)。每当您有一个函数调用new然后执行可能引发异常的其他操作(例如new再次调用)时,这变得很棘手。如果这发生在构造函数中,则不会调用对象的析构函数(尽管会调用基类和字段的析构函数),因此您无法像尝试对示例进行的那样在析构函数中进行清理。

幸运的是std::unique_ptr,存在使这更容易。如果您将异常类编写为:

struct xexception {
  std::unique_ptr<int[]> ttt[10];
  xexception() {
    ttt[0].reset(new int[0xfffffffL]);
    ttt[1].reset(new int[0xfffffffL]);
    ttt[2].reset(new int[0xfffffffL]);
    ttt[3].reset(new int[0xfffffffL]);
    ttt[4].reset(new int[0xfffffffL]);
    ttt[5].reset(new int[0xfffffffL]);
    ttt[6].reset(new int[0xfffffffL]);
    ttt[7].reset(new int[0xfffffffL]);
    ttt[8].reset(new int[0xfffffffL]);
    ttt[9].reset(new int[0xfffffffL]);
  }
};

它应该可以工作并且不会泄漏内存。

于 2013-04-06T17:34:14.173 回答
5

虽然我认为不使用std::string核心,基本例外可能是一个很好的指导方针,但我认为面向用户的库/应用程序不一定要遵循这一点。

可能还有其他原因,但您遇到了第一个原因:您想向用户(或开发人员)指示具有上下文意义的信息,而您通常无法仅使用文字字符串来做到这一点。为了做到这一点,必须进行动态分配。如果出于某种原因,你有一个bad_alloc,你可能已经开始使用软管,所以它不会给你买/失去任何东西。

编辑:

顺便说一句: 的析构函数std::exception被标记为虚拟是有原因的!

于 2013-04-06T01:14:59.453 回答