标准容器和算法的所有异常保证在哪里可以找到?

28
是的,我看过我能找到的C++标准(或者草案),但我没有找到STL容器所提供的完整的异常保证。所有我能找到的只是偶然的在一些函数和一些类型上不完整的描述。或许它就在那里,但我没找到,我也不知道。注意:并不要求列出人们能想到的所有保证,这基本上已经在这个问题中了。我正在寻找这些信息的权威来源本身——最好是一个免费版本的源(例如标准的草案),我可以将其作为正式的处理。

我在标准中也没有看到这个保证。你为什么认为这是有保证的?我不认为它是有保证的,而且元素类型是否可移动似乎与vector::swap无关,因为vector::swap需要在常数时间内运行,这意味着通常它不能进行逐个元素的交换。 - bames53
1
@bames53:我的错,那是个打错的字,我本来指的是 vector::insert(底层可能使用std::swap)。不管怎样,我并没有说这是保证的,我只是举了一个我正在寻找的潜在示例 - user541686
1
@bames53:真的吗?这有点琐碎...分配一个新容器,复制新数据(如果出现错误则销毁副本),然后移动旧数据。它可以工作;只是可能很慢,我想知道这是否有保证。如果没有,那就不行了。但像我说的:我只是举了一个潜在的例子...如果我已经知道答案,那我就不需要查找了! - user541686
不,例如适用于vector :: insert的注释:“如果非CopyInsertable T的移动构造函数引发异常,则效果未指定。” n3337,您链接到它,可以视为官方文件。如果您找不到某些内容使特定保证成立,则不保证。所有信息都在那里,只是分散在偶尔的部分中,每个部分本身都不完整。我认为您将不得不找到一些非权威参考资料,这些资料更容易阅读,或者只是学习标准及其组织方式。 - bames53
1
@bames53:我写这个问题的原因是我只能在标准的associative.reqmts.exceptunord.req.except部分找到答案,它们本身非常不完整。我甚至从未想过.modifiers部分会包含异常要求信息。所以是的,现在有了下面的答案,我可以看到很多信息都在那里,但当我发布这篇文章时,我找不到它。 - user541686
显示剩余2条评论
3个回答

18

阅读标准文档可能让人望而生畏(我们先回到标准上),但Bjarne Stroustrup在他的书《C++程序设计语言》附录中写了一篇关于此主题的非常好的文章。他在以下网址发布了这篇附录:

http://www.stroustrup.com/3rd_safe0.html ,以及 http://www.stroustrup.com/3rd_safe.pdf

这篇文章相当长且详细(并且写得很好)。例如,您可能会对E.4章节感兴趣,引用如下:

E.4 标准容器保证

如果库操作本身抛出异常,它可以 - 也确实 - 确保其操作对象被留在一个定义良好的状态。例如,对于向量(§16.3.3)抛出out_of_range的at()不是向量的异常安全性问题。at()的编写者没有任何问题,在抛出异常之前确保向量处于定义良好的状态。

此外,E.4.1章节指出:

除了基本保证外,标准库还针对一些插入或删除元素的操作提供了强异常安全保证。

请参阅第956页。其中包含了关于向量、双端队列、列表和映射的各种操作保证的表格。 总之,这些容器上的所有操作要么不抛出异常(nothrow),要么提供强异常安全保证,除了N - 元素插入到映射只提供基本保证。

注意:上述文本是旧的,不适用于C++11,但对于大多数目的而言仍然足够正确。

关于C++11...

标准首先关于以下容器声明:

array、deque、forward_list、list、vector、map、set、unordered_map、unordered_set、queue、stack

声明:

23.2.1/10:

除非另有规定(请参见23.2.4.1、23.2.5.1、23.3.3.4和23.3.6.5),否则此条款中定义的所有容器类型均满足以下附加要求:

— 如果插入单个元素时由insert()或emplace()函数引发异常,则该函数没有影响。
— 如果push_back()或push_front()函数引发异常,则该函数没有影响。
— 没有erase()、clear()、pop_back()或pop_front()函数会引发异常。
— 返回的迭代器的任何复制构造函数或赋值运算符不会引发异常。
— swap()函数不会引发异常。
— swap()函数不会使引用、指针或迭代器无效,这些引用、指针或迭代器是指向被交换容器的元素的。

上述各节中指出的问题(每个称为异常安全性保证)主要涉及特殊情况,如处理包含类型的哈希、比较操作以及抛出交换和抛出移动操作时的异常。


1
啊,一开始我没意识到,但是PDF似乎有我需要的信息(尽管我还需要检查一下)……它没有C++11的信息,但仍然很有用。:) 谢谢。+1 - user541686
这看起来好像不太符合我的需要……例如,std::rotate呢?它像std::copy吗?还是更强或更弱?(我意识到这在C++11和之前的C++03中可能有所不同——它可以使用任何组合的复制构造函数、移动构造函数、复制赋值函数、移动赋值函数和交换函数,具体取决于它是如何实现的。我需要知道它可以提供什么样的保证,无论是对于C++11还是C++03,但以C++11之前的版本为起点也是一个好的开始。) - user541686
1
对于算法来说并不是那么清楚。我建议您列出您感兴趣的方法,并发布一个新问题,标题为“std算法的异常安全性”。这些算法使用swap、insert等操作,因此它们不能比底层容器更好,但您需要在标准中阅读每个算法的要求。例如,对于rotate,容器元素需要可交换等。就我所知,在swap在rotate操作中抛出异常的情况下,没有明确的声明保证。我认为这是“弱”的,但您可以尝试提出一个新问题。 - Johan Lundberg
好的,我想我会按照当前STL实现中所看到的内容来处理附录中没有涉及到的部分,并基于此提供保证...如果需要的话,我稍后会有更多问题。那个PDF很棒! - user541686

11

n3376

23.2.1 通用容器要求 [container.requirements.general]

第10段

除非另有规定(参见23.2.4.1、23.2.5.1、23.3.3.4和23.3.6.5),本款中定义的所有容器类型均满足以下附加要求:
- 如果在插入单个元素时通过insert()或emplace()函数抛出异常,则该函数没有效果。
- 如果通过push_back()或push_front()函数抛出异常,则该函数没有效果。
- 没有erase()、clear()、pop_back()或pop_front()函数会抛出异常。
- 由返回的迭代器的任何复制构造函数或赋值运算符都不会抛出异常。
- 任何swap()函数都不会抛出异常。
- 任何swap()函数都不会使引用、指针或迭代器失效,这些引用、指针或迭代器是指向被交换容器的元素。
[注:end()迭代器不引用任何元素,因此可能会失效。——注释]

23.2.4 关联容器 [associative.reqmts]

23.2.4.1 异常安全性保证 [associative.reqmts.except]

1 对于关联容器,没有clear()函数会抛出异常。除非由容器的比较对象(如果有)抛出异常,否则erase(k)不会抛出异常。
2 对于关联容器,如果在插入单个元素时从insert或emplace函数内部抛出异常,则插入没有效果。
3 对于关联容器,除非容器的比较对象(如果有)的交换引发异常,否则任何swap函数都不会引发异常。

23.2.5 无序关联容器 [unord.req]

23.2.5.1 异常安全性保证 [unord.req.except]

1 对于无序关联容器,没有clear()函数会抛出异常。除非由容器的哈希或Pred对象(如果有)抛出异常,否则erase(k)不会抛出异常。
2 对于无序关联容器,如果从insert或emplace函数内部除容器的哈希函数以外的任何操作抛出异常,则插入没有效果。
3 对于无序关联容器,除非容器的Hash或Pred对象(如果有)的交换引发异常,否则任何swap函数都不会引发异常。
4 对于无序关联容器,如果在rehash()函数内部除了由容器的哈希函数或比较函数引发的异常之外还有其他异常,则rehash()函数没有效果。

23.3.3.4 deque修改器[deque.modifiers]

void push_back(T&& x); 第2段

注意:如果T的复制构造函数、移动构造函数、赋值运算符或移动赋值运算符以外的异常被抛出,则没有任何影响。如果非CopyInsertable T的移动构造函数抛出异常,则效果是未指定的。

iterator erase(const_iterator first, const_iterator last); 第6段

除非T的复制构造函数、移动构造函数、赋值运算符或移动赋值运算符抛出异常,否则不会抛出任何异常。

23.3.6.5 vector modifiers [vector.modifiers]

void push_back(T&& x); 第2段

如果非CopyInsertable T的移动构造函数抛出异常,则效果是未指定的。

iterator erase(const_iterator first, const_iterator last); 第5段

除非T的复制构造函数、移动构造函数、赋值运算符或移动赋值运算符抛出异常,否则不会抛出任何异常。


嗯...所以,例如,如果我调用vector::insert()在10000个元素的向量中的索引9998处插入2个元素,并且对象的复制构造函数可能会抛出异常,那么向量是否会将所有10000个元素复制到其他地方以确保安全? - user541686
@Mehrdad:你错过了这个“在插入单个元素时”的部分。 - Martin York
不,这正是我问你的原因。如果我只插入一个元素,那么答案已经在你的帖子中了。我正在寻找一些综合性的东西——可以解释例如上述情况中会发生什么。 - user541686
1
@Mehrdad:你好像误解了我的回答。除了基本的保证外,2没有任何保证。它只在“插入单个元素”时提供保证。 - Martin York
那么你的意思是,当插入多个元素时,复制构造函数抛出异常会对容器造成严重破坏并可能导致资源泄漏? - user541686
显示剩余4条评论

2
您链接的文档,即n3337草案标准,可以视为正式标准。它是C++11标准加上微小的编辑更改。
您只需要学习阅读标准即可,这很容易理解,因为它并不打算成为易读材料。
要查找特定的库操作的异常保证,请检查该操作的规范以获取异常的备注和注释。如果函数是成员函数,则请检查类型的规范以了解有关异常安全性的评论以及它满足的要求。然后检查已满足的要求以了解对象必须做出哪些异常保证才能满足这些要求。
对于通用类型和算法,还需检查放置在模板参数上的要求,以了解这些类型必须满足哪些要求,以便所有由类型、算法或成员函数提供的异常保证得以保持(如果模板参数未满足指定的要求,则使用这些参数使用模板具有未定义的行为,并且不应用任何模板规范)。

你知道“修饰符”是什么意思吗?当我看到那里的异常保证时,它完全让我困惑了。 - user541686
@Mehrdad 标准将每个类的成员函数分为不同的类别。如果您查看类概述,您将看到所有函数按类别划分的列表。每个组在开头都有一个链接,可以带您进入指定该组函数的部分。 - bames53
例如,第23.3.6.0节概述了向量类模板,如果您向下滚动,您会看到一个注释"//23.3.6.3 capacity",后面跟着与向量大小和容量相关的函数。如果您跟随链接到第23.3.6.3节,您将找到该组中每个函数的规格,包括有关函数提供的任何异常保证的信息。例如,对于vector::reserve(),指定“如果抛出除非非CopyInsertable类型的移动构造函数之外的异常,则没有效果。” - bames53
@Mehrdad 换句话说,“修饰符”部分意味着“标准已分类为修饰符的函数”。它与异常无关,您也会在其他组中找到异常信息,例如“构造函数、复制和赋值”、“容量”、“数据”、“专业算法”等。 - bames53
哦...所以“modifiers”只是指mutators?这很有道理(除了resizeat不是modifiers之外)...哇,这样的阻抗不匹配!感谢详细的解释! - user541686
@Mehrdad,resize()和at()不属于“修饰符”类别。resize()属于“容量”(23.3.6.3),而at()属于“元素访问”。 - bames53

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