C++/STL的方法异常保证信息在哪里可以找到?

9

最近我在写带有异常处理的代码,对于异常、它们的保证和可抛出性质,我有一些疑问。

基本上,假设你有以下代码:

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 &)中包裹代码吗?如果这样做,stringifstream能够保证强异常安全性,确保没有资源泄漏和未关闭的文件吗?
另外,如果我的类抛出从exception派生的myexception,那么catch(exception &)似乎会让它通过。所以我只能用catch(...),但我记得它会捕获访问冲突?还有其他方法吗?
然后,有一个信息说如果对象构造函数的子构造函数引发异常,任何异常都不应该被捕获,构造函数应该在任何成员对象引发异常时抛出异常。
如果上面的代码不是从构造函数而是从常规函数void foo()调用的,我应该捕获哪些异常?outofmemory_something、filenotfound_something?我在哪里可以找到STL对象可能引发的定义?它们是特定于实现的吗?
我在哪里可以找到权威来源来解决我对此话题的所有疑虑和问题?
到目前为止,处理异常就像在一堆粘液中跳舞。错误码似乎要简单得多且更安全...

4
为什么不呢?我一直认为那是很好的。它是RAII的重要组成部分。 - R. Martinho Fernandes
3
@Werolik说错了,非常错误。整个C++标准(包括核心语言和库)都是为此精心设计的。另一方面,错误返回值并不是C++的设计要素。例如,构造函数甚至不能返回错误代码。 - MSalters
3
@n.m.: 然后可能会由于内存不足、访问冲突等问题而引发错误?对于系统状态我没有什么头绪,堆是否被破坏、堆碎片化等等,基本保证?强保证?我可以在对象外部使用catch(..)来清理,然后重试,但是我不知道系统是否仍然稳定……也找不到更多相关资料的来源,除了那些不具可移植性且难以理解的源代码。 - Coder
1
@Werolik:然后你又会错了,初始化列表中的对象也可以从它们的构造函数中抛出异常。 - Konstantin Oznobihin
2
@Werolik:我之前说的仍然适用。初始化列表并不改变任何事情。 - R. Martinho Fernandes
显示剩余13条评论
5个回答

6
如果其中任意一个抛出异常
class X {
string m_str;
X() : m_str("foo")//what if this throws?
{
    ifstream b("a.in")//what if this throws?
}

如果对象的构造函数抛出异常,则所有完全创建的成员都将被销毁(使用它们的析构函数),并将对象的内存返回给系统。因此,在抛出点未完全构建的任何成员将不会被销毁(因为它们尚未被创建)。
  • 如果在初始化列表中m_str()抛出异常,则对象将永远不存在。
  • 如果在函数体中ifstream抛出异常,则m_str将被销毁,对象将永远不存在。

我应该将代码包装在catch(exception&)中吗?即使这样做,string和ifstream是否保证强大的保证,即没有泄漏资源,也没有留下半开放的东西?

即使您捕获异常(在对象外部),由于对象从未存在过(对象仅在构造函数完成后开始其生命周期),因此没有对象可用于工作。
在上述情况下,保证没有泄漏或打开的资源。

此外,如果我的类引发了派生自exception的myexception,catch(exception &)似乎会让它通过。那么我只能用catch(...),但我IRC捕获访问冲突?是否还有其他方法?

如果您的异常源自std::exception,那么catch(std::exception&)将起作用。如果它不起作用,则您正在做某些错误的事情(但我们需要更多详细信息(例如抛出代码和捕获代码,英文描述不足))。
然后在某个地方有一条信息,即对象构造函数的子构造函数中抛出的任何异常都不应该被捕获,如果成员对象中有任何一个抛出,则构造函数应该抛出。
这可能是最好的选择,也是一般规则不错的建议。
如果上面的代码不是从构造函数而是从常规函数void foo()调用的,我应该捕获哪些异常?outofmemory_something、filenotfound_something吗?在哪里可以找到STL对象可以抛出什么的定义?它们是特定于实现的吗?
只有在您能够处理异常时才应该捕获它们。通常这是无法做到的,所以不要捕获它们,让应用程序正常退出(通过异常展开堆栈)。
我可以在哪里找到权威的来源来清除我对这个主题的所有疑问和问题?
您的问题如此之多,以至于很难回答。 我可以推荐Herb Sutter的《"Exceptional C++》。 > 到目前为止,处理异常就像在一堆黏糊糊的东西中跳舞一样。 错误代码似乎简单得多且更安全...
您错了。 异常要容易得多。 您似乎在过度思考并感到困惑。 这并不是说错误代码没有用处。
如果出现问题并且您无法在本地修复它,那么抛出异常。 所有标准库中的类都是以异常为设计目的,并且将正确运行。 这样只留下您的类。

规则:(适用于您的对象)

  • 确保您的类在析构函数中清理自己
  • 如果您的对象包含资源,请确保遵守“三法则”
  • 永远不要让一个对象拥有多个资源。
    注意:您可以拥有多个像std::string或std::ifstream这样的东西,因为它们控制资源(它们每个控制一个资源,所以您的类不控制该资源)。 资源(在此上下文中)是必须手动创建/销毁的东西。

就这样,其他都是自动完成的。


有用的,但请引用来源。 - Jason S
但很有可能“a.in”是在系统目录下创建的,而m_str破坏了堆栈? - Coder
@程序员:如果在系统目录中创建了“a.in”文件(并且流已设置为在出现错误时抛出异常),则它将抛出异常,不会发生任何错误。如果您有用户界面,则可能需要捕获此异常并向用户报告,但否则一切都将很好。 - Martin York
@程序员:m_str不可能破坏堆栈。如果它失败,它将工作或抛出异常。除非存在错误,否则堆栈将保持正常。(请注意,如果存在错误,则无法从任何代码中获得保证)。 - Martin York
@Jason S:有什么特别要求吗? - Martin York

3
每个函数都有前置条件和后置条件。抛出异常的正确时机是当无法满足后置条件时。没有其他正确的时机。
有两种特殊情况。
- 构造函数的后置条件是存在一个有效对象,因此抛出异常是报告错误的唯一合理方式。如果您有类似于 `Foo::is_ok()` 的测试,则表示一个代表无效状态的有效对象。 - 析构函数的后置条件是不存在对象,因此抛出异常永远不是报告错误的合理方式。如果您需要在对象生命周期结束时执行复杂操作,请将其作为单独的 `Foo::commit()` 成员函数调用。
除此之外,您还有选择,并且这是品味的问题。
例如:
- `std::vector::operator[]` 不检查前置条件并且具有 `noexcept(true)`,但是 - `std::vector::at()` 进行检查并抛出异常。
选择是你是否认为你的先决条件是有效的。在第一种情况下,您正在使用按合同设计。在第二种情况下,假设您已经检测到它们无效,您知道后置条件不能有效,因此应该抛出异常;在第一种情况下,您假定它们是有效的,并且在此基础上,后置条件必须有效,因此您永远不需要抛出异常。
GOTW涵盖了很多异常的黑暗角落,并演示了为什么事情是什么样子

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在其析构函数中关闭文件。


1

关于标准库的工作原理,包括在哪些条件下允许抛出哪些异常类型,唯一权威的参考资料是C++语言标准。多年前,它以电子形式以合理的价格提供,但不幸的是现在似乎不再如此。您可以考虑在标准委员会网站上搜索草案,但与已发布的标准肯定存在差异。

还要注意,新版标准刚刚发布,厂商实现新功能的合理完整性需要一些时间。


大多数常见的编译器已经开始实现新标准的功能。尽管如你所说,他们完成这些功能可能需要一段时间:http://gcc.gnu.org/projects/cxx0x.html。请参考:https://dev59.com/wnVD5IYBdhLWcg3wHnyd#4653479。 - Martin York

0
据我所知,抛出异常与否(更重要的是哪些异常)大多数情况下取决于实现。我不认为有任何意义去尝试捕获它们 - 我的意思是,如果这个失败了,你会做什么?有没有合理的方法从抛出异常中恢复过来?
请记住,如果例如无法打开文件,则不会抛出任何异常 - 这只会导致流被设置为失败状态。

1
“是否抛出异常(更重要的是,哪些异常)大多由实现决定。”——这不是真的。标准中没有这样的规定。 - n. m.
流可以在“异常模式”下正常运行。这只是不是默认模式。 - sehe
2
@n.m:请参阅C++11 17.6.5.12/4:“除非另有规定,否则在C++标准库中定义的任何其他未具有异常说明的函数都可能抛出实现定义的异常。” C++03 17.4.4.8/3中也存在相同情况。 - Konstantin Oznobihin
是的,我忘了那个。我想说的是,当标准规定它们应该抛出异常时,不允许库函数不抛出异常。 - n. m.
嗯,我想这很类似于说它大多数情况下取决于实现。 - Konstantin Oznobihin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接