【问题标题】:std exceptions inviting unsafe usage?引发不安全使用的标准异常?
【发布时间】:2011-09-06 11:06:55
【问题描述】:

建议你总是抛出一些源自std::exception 的东西,并且有一些预定义的特化,例如std::runtime_error

std::exception 的接口以非抛出访问器的形式给出。伟大的。现在看std::runtime_error的构造函数

class runtime_error : public exception {
public:
  explicit runtime_error (const string &);
};

所以如果我这样做这个

try {
    foo ();
}
catch (...) {
    throw std :: runtime_error ("bang");
}

foo 完全有可能因为内存不足而抛出,在这种情况下,构造 runtime_errorstring 参数也可以抛出。这将是一个本身也会抛出的 throw 表达式:这不会调用 std::terminate 吗?

这是否意味着我们应该始终这样做:

namespace {
    const std :: string BANG ("bang");
}

...

try {
    foo ();
}
catch (...) {
    throw std :: runtime_error (BANG);
}

但是等等,这也行不通,是吗?因为runtime_error 会复制它的参数,这也可能抛出......

...所以这是否意味着没有安全的方法来使用 std::exception 的标准特化,并且您应该始终滚动自己的字符串类,其构造函数只会失败而不抛出?

或者我缺少什么技巧?

【问题讨论】:

  • 使用具有小字符串优化(无分配)和非常短的错误消息的标准库实现?

标签: c++ exception exception-handling constructor std


【解决方案1】:

我认为您的主要问题是您正在执行 catch(...) 并转换为 std::runtime_error 从而丢失原始异常中的所有类型信息。你应该用throw()重新抛出。

实际上,如果您的内存不足,您可能会在某个时候抛出bad_alloc 异常,并且您可以(或应该)做的事情不多。如果您想要由于分配失败以外的原因引发异常,那么在构造具有有意义的上下文信息的合理异常对象时不太可能遇到问题。如果您在格式化异常对象时遇到内存问题,除了传播内存错误外,您无能为力。

如果你构造一个新的字符串对象来构造一个异常,你是对的,但是如果你想用上下文格式化一个消息,这通常是无法避免的。请注意,标准异常对象都有一个 const char* 构造函数(截至上周),因此如果您有一个要使用的 const char*,则不必构造一个新的 std::string 对象。

std::runtime_error 必须复制它的参数,但不一定作为一个新的字符串对象。可能有一块静态分配的内存区域,它可以将其参数的内容分配给该区域。它只需要满足what() 的要求,只需要返回一个const char *,它不必存储一个std::string 对象。

【讨论】:

  • 好吧——在某些情况下你可以释放内存并希望它没有碎片化。还可能会指出,某些系统可能会过度使用内存,因此当进程用尽虚拟内存时将引发异常(在 64 位系统的情况下,这意味着永远不会使用“仅”48 位地址空间,以防万一32 位的它通常是 3GiB 假设 1GiB 的高内存)。
  • @MaciejPiechotka:在系统过度使用的情况下,不太可能抛出 C++ 异常 - 至少不会以任何符合要求的方式 - 因为内存“错误”只会在不允许抛出异常的点。
  • 上升。抱歉 - 缺少“仅”。当虚拟内存耗尽时,IIRC 每个系统都会抛出异常,而过度使用通常只会在那时抛出(尽管它可能会终止进程)。我的评论似乎暗示 vm 耗尽的异常只会在过度使用 OS 时引发,这当然是没有意义的。
【解决方案2】:

这将是一个 throw 表达式,它本身也会抛出:won't this 会调用 std::terminate 吗?

不,不会。它只会抛出有关内存不足的异常。控件不会到达外部throw 部分。

但是等等,这也行不通,是吗?因为 runtime_error 是 将复制它的参数,这也可能抛出......

带有抛出复制构造函数的异常类与抛出析构函数一样邪恶。对此无能为力。

【讨论】:

    【解决方案3】:

    std::runtime_error 旨在处理常见的运行时错误,而不是 内存不足或其他此类严重异常。基类 std::exception做任何可能抛出的事情;也不 std::bad_alloc。显然,将std::bad_alloc 重新映射为 需要动态分配才能工作的异常是一个坏主意。

    【讨论】:

      【解决方案4】:

      如果因为内存不足而碰巧遇到 bad_alloc 异常,您会怎么做?

      我想说的是,在经典的 C++ 程序中,您希望程序以某种方式试图告诉您发生了什么然后终止。

      在经典的 C++ 程序中,您会让 bad_alloc 异常传播到程序的主要部分。 main 将包含这样的 try/catch 排列:

      int main()
      {
         try
         {
            // your program starts
         }
         catch( const std::exception & e )
         {
             std::cerr << "huho something happened" << e.what() << std::endl;
         }
         catch( ... )
         {
             std::cerr << "huho..err..what?" << std::endl;
         }
      }
      

      您只会在线程的主函数和启动函数中使用 catch( ... )。与 Java 等其他一些语言相反,您不需要在本地捕获所有可能的异常。您只需让它们传播,直到您将它们捕获到您想要的位置。

      现在,如果您的代码必须特别检查 std::bad_alloc,您应该只在本地 catch( const std::bad_alloc & )。在那里,做其他事情而不是仅仅重新抛出另一个异常可能是明智的。

      我在 The C++ Programming Language §14.10 中还发现 C++ 异常处理机制为自己保留了一些内存来保存异常,因此抛出标准库异常不会自己抛出异常。当然,如果你真的编写了一些变态的代码,也可能让异常处理机制耗尽内存。

      所以,总而言之,如果你什么都不做,让像 bad_alloc 这样的大异常在你想要捕获它们的地方很好地传播,我认为你是安全的。除了主函数和线程的启动函数之外,您不应在任何地方使用 catch( ... )catch(const std::exception & )

      捕获所有异常以重新抛出单个异常实际上是最后要做的事情。您失去了使用 C++ 异常处理机制获得的所有优势。

      【讨论】:

        猜你喜欢
        • 2011-05-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-01-04
        • 1970-01-01
        相关资源
        最近更新 更多