前几天我正在编写带有异常处理的代码,并且我有一些关于异常,保证和抛弃的问题。
基本上,你说:
class X {
string m_str;
X() : m_str("foo")//what if this throws?
{
ifstream b("a.in")//what if this throws?
}
在浏览了我能找到的所有文章之后,我仍然不知道处理这个问题的方法是什么。
说我有一个代码:
{
...
X myInstanceOfClassX;
...
}
我应该将代码包装在catch(exception &)
中吗?如果我这样做,string
和ifstream
是否保证有强有力的保证,没有资源被泄露而且没有任何东西被打开一半?
此外,如果我的类抛出myexception
,这是从异常派生的,catch(exception &)
似乎让它通过。那么我留下了catch(...)
哪个IIRC可以捕获访问权限?还有另一种方式吗?
然后在某处有一条信息,不应该捕获在对象构造函数的子构造函数中抛出的任何异常,并且构造函数应该抛出以防任何成员对象抛出。
如果上面的代码不是从构造函数调用的,而是从常规函数void foo()
调用的,那么我应该捕获哪些异常呢? outofmemory_something,filenotfound_something?我在哪里可以找到STL对象可以抛出的定义?它们是实施具体的吗?
权威来源在哪里可以清除我对此主题的所有怀疑和疑问?
到目前为止,处理异常似乎就像在一大堆gooo中跳舞一样。错误代码似乎更简单,更安全......
答案 0 :(得分:5)
如果其中任何一个抛出
class X {
string m_str;
X() : m_str("foo")//what if this throws?
{
ifstream b("a.in")//what if this throws?
}
然后您创建的对象将不存在 如果在对象的构造函数中抛出异常,则会破坏所有完全创建的成员(使用其析构函数),并将对象的内存返回给系统。因此,任何未在抛出点完全构造的构件都不会被销毁(因为它们尚未被创建)。
我应该将代码包装在catch(exception&)中吗?如果我这样做,字符串和ifstream是否保证有一个强有力的保证,没有资源泄露,没有任何东西被打开一半?
即使你捕获异常(在对象之外),也没有对象可以工作,因为它从未存在过(对象只在构造函数完成后才开始其生命周期)。
在上文中,您可以保证没有泄漏或开放资源。
此外,如果我的类抛出myexception,即派生自异常,catch(exception&)似乎让它通过。所以这让我陷入了IIRC捕获访问权限的问题(?)?还有另一种方式吗?
如果您的异常派生自std :: exception,则catch(std::exception&)
将起作用。如果它不起作用那么你做错了(但我们需要更多的细节(比如抛出的代码和捕获的代码,英文描述是不够的))。
然后在某处有一条信息,不应该捕获在对象构造函数的子构造函数中抛出的任何异常,并且构造函数应该抛出以防任何成员对象抛出。
可能是最好的选择,作为一般规则也不错的建议。
如果上面的代码不是从构造函数调用的,而是从常规的函数void foo()调用,我应该捕获哪些异常呢? outofmemory_something,filenotfound_something?我在哪里可以找到STL对象可以抛出的定义?它们是实施具体的吗?
如果你能做些什么,你应该只捕捉异常。通常这没什么,所以不要抓住它们让应用程序正常退出(通过异常展开堆栈)。
权威来源在哪里可以清除我对此主题的所有怀疑和疑问?
你的问题是如此多变以至于很难 我可以推荐"Exceptional C++" by Herb Sutter。
到目前为止,处理异常似乎就像在一大堆gooo中跳舞一样。错误代码似乎更简单,更安全......
你错了。例外情况要容易得多。你似乎只是过度思考它而感到困惑。这并不是说错误代码没有它们的位置。
如果出现问题and you can not fix it locally
,则抛出异常。标准中的所有类都是在设计时考虑到例外并且行为正确。所以这就是你的课程。
就是这样,其余的自动神奇地起作用。
答案 1 :(得分:3)
每个函数都有前置条件和后置条件。抛出异常的正确时间是无法满足后置条件的时间。没有其他正确的时间。
有两种特殊情况。
构造函数的后置条件是存在有效对象,因此throw是 only 报告的合理方式错误。如果您有类似Foo::is_ok()
测试的内容,那么您拥有的是一个有效的对象,代表无效状态。
析构函数的后置条件是对象的不存在,因此抛出 never 是报告错误的合理方式。如果你在对象的生命结束时有一些棘手的事情要做,那就把它作为一个单独的Foo::commit()
成员函数调用。
除此之外,你有选择权,这是一个品味问题。
例如
std::vector::operator[]
不会检查前置条件,而是noexcept(true)
,但std::vector::at()
会检查并抛出。选择是否假设您的前提条件有效。在第一种情况下,您使用的是按合同设计。在第二种情况下,给定你已经检测到它们不是,你知道后置条件不能有效,因此应该< / em> throw;在第一种情况下,你假设他们是给予,后置条件必须有效,因此你永远不会需要。
GOTW涵盖了很多异常的黑暗角落,很好地展示了why things are what they are。
答案 2 :(得分:1)
关于标准库如何工作的唯一权威参考,包括允许抛出哪些异常类型的条件,是C ++语言标准。几年前它以合理的价格以电子形式提供,但不幸的是,这似乎不再是这种情况。您可以考虑在Standard Committee site搜索草稿,但显然会与已发布的标准存在差异。
另请注意,该标准的新版本刚刚发布,供应商需要一段时间才能实现合理完整性的新功能。
答案 3 :(得分:1)
在构造函数中抛出异常是一个好主意,因为您没有其他报告失败的方法。
我倾向于更喜欢错误代码的C ++异常,即使它们被认为是“控制流”,因为我不必在任何地方添加检查。但这是一个有争议的品味问题。对于构造函数,你别无选择。
一旦构造函数抛出异常,所有初始化的子对象都会被销毁,如果对象是通过operator new
构造的,则会调用相应的operator delete
。
请注意,当构造函数抛出时,无法使用该对象:
my_class a; // If this throws, everything past this line is not accessible.
// Therefore, you cannot use a.
或
my_class* b;
try
{
b = new my_class; // If this throws, ...
}
catch (...)
{
// b has undefined state here (but no memory is leaked)
}
因此,如果您只使用正确的RAII对象,那么除了让异常传播之外,您是安全的并且无所事事。但是,如果您手动检索一次性资源,则可能需要清理它并重新抛出异常:
template <typename T>
struct my_vector
{
// This is why it is not advisable to roll your own vector.
my_vector(size_t n, const T& x)
{
begin = static_cast<T*>(custom_allocator(n * sizeof(T)));
end = begin + n;
size_t k = 0;
try
{
// This can throw...
for (; k != n; k++) new(begin + k) T(x);
}
catch (...)
{
// ... so destroy everything and bail out
while (--k) (begin + k)->~T();
custom_deallocator(begin);
throw;
}
}
private:
T* begin;
T* end;
};
但是如果你使用正确的RAII对象,这应该是非常罕见的(我当前代码库中的快速grep
显示了数百个throw
,但只有两个catch
。
标准库中的异常保证可以在ISO标准文档中找到(您只需支付少量费用)。
此外,任何优秀的C ++书籍都会详细讨论异常安全问题,关键是通常你没有什么特别的事情要做。例如,在您的示例中,所有内容都将正确处理,因为ifstream
会在其析构函数中关闭文件。
答案 4 :(得分:0)
AFAIK是否(以及更重要的是,哪些)异常被抛出主要留给实现。我没有看到试图捕捉那些的任何意义 - 我的意思是,如果失败你会怎么做?是否有任何合理的方法可以从抛出的异常中恢复?
请记住,例如,如果无法打开文件,则不会引发异常 - 这只会导致将流设置为失败状态。