3
  1. STL 容器元素是否需要具有noexcept复制构造函数和复制赋值运算符?如果可能,请提供参考。
  2. 如果不是,当在多插入期间发生异常时,STL 容器的状态是什么,例如在填充插入期间。

尝试编写允许拦截/否决修改的通用包装器时会出现问题。我能想到的任何实现都可能会改变底层容器的语义,除非专门针对每种容器类型(这不是一个真正的选择)。

例如,std::vector有一个填充插入

void insert (iterator position, size_type n, const value_type& val);

这需要value_type同时是CopyInsertableCopyAssignable。请注意,它不需要值类型为DefaultConstructible

编辑 3 Stroustrup本人(第 956 页上的表格)表示多元素插入应该对所有向量、双端队列、列表和映射都有强有力的保证。这意味着完整的标准库操作会自动成功或失败。

编辑 4但是,该保证仅适用于相关操作(在本例中为复制构造函数)本身不引发异常的情况,这正是我的问题。

据我了解,这留下了两种基本的实现方法:

  1. 为新元素创建虚拟val条目并复制分配。这仅在可以通过复制容器中的现有元素或DefaultConstructible(这不是必需的)value_type来创建虚拟元素时才有效。
  2. 一个接一个地复制构造元素到容器中它们各自的位置。这似乎或多或少是规范的实现。

编辑 2:我不会将此称为未定义行为,因为该术语似乎使人们认为未定义为由语言运行时/标准定义

当复制构造函数或复制赋值运算符引发异常时,这两种实现似乎都会使容器留下未知内容(即不清楚容器在异常之后包含哪些元素)。

编辑 1:请注意,这并不意味着我认为 C++ 运行时存在不良行为,例如内存泄漏或未定义的值。但是,似乎或多或少没有说明容器的内容是什么。特别是,容器的内容可能已经完全(尽管始终如一地)改变。

例如,考虑第三种(混合)方法:

  1. 创建n模板对象的副本列表val
  2. 将此列表中的元素复制分配到目标容器中。

不同之处在于复制构造函数引发异常时对容器的影响。在这种情况下,如果复制构造函数抛出,容器的内容将保持不变(但当复制赋值运算符抛出时仍会导致未指定的内容)。使用指针时(即不使用时std::vector),复制赋值可能会被忽略,只有指针重新排列,从而使操作原子化。例外。

至于noexcept容器元素:对象是通过创建的allocator_traits<value_type>::construct(ptr, args),这不是noexcept,我也找不到容器元素大多数具有noexcept复制构造函数/复制分配运算符的要求(例如std::shared_ptrstd::unique_ptr需要这个)。

请注意,复制构造和复制分配的自动生成操作必须是noexcept,除非它们需要调用本身可能引发异常的操作。

这让我感到困惑,我确定我错过了一些东西,但我无法弄清楚标准中可能证明我错了的部分。

4

1 回答 1

5
  1. STL 容器元素是否需要具有 noexcept 复制构造函数和复制赋值运算符?如果可能,请提供参考。

不,它们不是,我无法为不存在的需求提供参考,因为它不存在。

如果需要,它会在标准中这样说,但事实并非如此。

一般来说,元素甚至根本不需要复制构造函数,对于大多数操作来说,移动构造函数就足够了。复制分配也是如此。

  1. 如果不是,当在多插入期间发生异常时,STL 容器的状态是什么,例如在填充插入期间。

这取决于容器,以及您在容器中插入的位置。

对于基于节点的容器,例如列表,如果一个插入抛出,那么任何已经插入的元素都保留在容器中。

究竟会发生什么取决于您在std::vector向量中插入的位置以及是否需要重新分配,以及元素是否具有 noexcept 移动构造函数。您可以依赖的是不会有泄漏和部分构造的元素,因此向量处于有效状态。

当复制构造函数或复制赋值运算符引发异常时,这两种实现似乎都使容器处于未定义状态(即不清楚容器在异常之后包含哪些元素) 。

这不是一个未定义的状态,它只是一个你没有完整信息的状态。您可以使用vector::size()andvector::capacity()来确定其状态,并检查该位置的元素[0, size())以检查元素的状态。之后,您就可以了解有关向量状态的所有信息。

即向量始终处于有效状态,您只是不知道足够的信息来准确描述该状态,直到您检查它。

“未定义”一词表明状态不一致或不可知,这是不正确的。标准容器始终提供基本的异常安全保证,因此失败的操作不会使它们处于未定义状态。

即使在极端情况下,例如vector::push_back()元素类型不可复制并且具有抛出的移动构造函数,异常也会使向量处于“未指定”状态,因此没有泄漏,没有无效对象,也没有未定义的行为。

例如,考虑第三种(混合)方法:

  • 创建模板对象 val 的 n 个副本的列表。
  • 将此列表中的元素复制分配到目标容器中。

不同之处在于复制构造函数引发异常时对容器的影响。在这种情况下,如果复制赋值运算符抛出异常,容器的内容将保持不变。

也许我误解了,但我不明白这比“一个接一个地复制构造元素直接到容器中它们各自的位置”更好。它执行了相当多的工作,执行 N 复制构造加上 N 复制分配,而不是 N 复制构造,并且在容器的最终状态方面我看不到任何优势。

在这两种情况下,您都需要向n容器中添加新元素,这可能会抛出,如果它抛出,我不明白为什么它会对最终状态产生任何影响,无论您是否还计划在之后进行一些额外的分配。

于 2015-08-24T10:36:58.330 回答