为什么没有容器移动赋值运算符noexcept?

时间:2012-09-08 17:07:40

标签: c++ c++11

我注意到std::string的(真正std::basic_string)移动赋值运算符为noexcept。这对我来说很有意义。但后来我发现没有标准容器(例如std::vectorstd::dequestd::liststd::map)声明其移动赋值运算符noexcept。这对我来说没什么意义。例如,std::vector通常实现为三个指针,指针当然可以移动分配而不会抛出异常。然后我想也许问题是移动容器的分配器,但std::string也有分配器,所以如果这是问题,我希望它会影响std::string

那么为什么std::string移动赋值运算符noexcept,但标准容器的移动赋值运算符不是?

3 个答案:

答案 0 :(得分:22)

我相信我们正在考虑标准缺陷。 noexcept规范(如果要应用于移动赋值运算符)有点复杂。无论我们是在谈论basic_string还是vector,我都相信这种说法是正确的。

根据[container.requirements.general] / p7,我对容器移动赋值操作符应该做的英文翻译是:

C& operator=(C&& c)
  

如果是alloc_traits::propagate_on_container_move_assignment::value   true,转储资源,移动分配分配器和转移   来自c的资源。

     

如果   alloc_traits::propagate_on_container_move_assignment::valuefalse   和get_allocator() == c.get_allocator(),转储资源和转移   来自c的资源。

     

如果   alloc_traits::propagate_on_container_move_assignment::valuefalse   和get_allocator() != c.get_allocator(),移动分配每个c[i]

注意:

  1. alloc_traits是指allocator_traits<allocator_type>

  2. alloc_traits::propagate_on_container_move_assignment::valuetrue时,可以指定移动分配运算符noexcept,因为它将要释放当前资源,然后从源中窃取资源。同样在这种情况下,分配器也必须移动分配,并且移动分配必须为noexcept,容器的移动分配必须为noexcept

  3. alloc_traits::propagate_on_container_move_assignment::valuefalse时,如果两个分配器相等,那么它将与#2做同样的事情。但是,在运行时之前,人们不知道分配器是否相等,因此您不能以此noexcept为基础。

  4. alloc_traits::propagate_on_container_move_assignment::valuefalse时,如果两个分配器相等,则必须移动分配每个单独的元素。这可能涉及向目标添加容量或节点,因此本质上是noexcept(false)

  5. 总结如下:

    C& operator=(C&& c)
            noexcept(
                 alloc_traits::propagate_on_container_move_assignment::value &&
                 is_nothrow_move_assignable<allocator_type>::value);
    

    我认为上述规范中没有依赖C::value_type因此我认为它应该同样适用于std::basic_string,尽管C ++ 11另有规定。

    <强>更新

    在下面的评论中,Columbo正确地指出事情一直在变化。我上面的评论与C ++ 11相关。

    对于草案C ++ 17(此时看起来似乎很稳定),情况有所改变:

    1. 如果alloc_traits::propagate_on_container_move_assignment::valuetrue,则规范现在要求allocator_type的移动分配不抛出异常(17.6.3.5 [allocator.requirements] / p4)。因此,不再需要检查is_nothrow_move_assignable<allocator_type>::value

    2. alloc_traits::is_always_equal已添加。如果这是真的,那么可以在编译时确定上面的第3点不能抛出,因为资源可以被转移。

    3. 所以容器的新noexcept规范可以是:

      C& operator=(C&& c)
              noexcept(
                   alloc_traits::propagate_on_container_move_assignment{} ||
                   alloc_traits::is_always_equal{});
      

      而且,对于std::allocator<T>alloc_traits::propagate_on_container_move_assignment{}alloc_traits::is_always_equal{}都是正确的。

      现在,在C ++ 17草案中,vectorstring移动赋值完全noexcept规范。但是,其他容器带有此noexcept规范的变体。

      如果您关心此问题,最安全的做法是测试您关心的容器的显式特化。对于VS,libstdc ++和libc ++,我已经为container<T>做了完全相同的事情:

      http://howardhinnant.github.io/container_summary.html

      这项调查大约有一年的历史,但据我所知仍然有效。

答案 1 :(得分:9)

我认为这样做的原因就是这样。

basic_string仅适用于非数组POD类型。因此,他们的破坏者必须是微不足道的。这意味着如果您为移动分配执行swap,那么移动到字符串的原始内容尚未被销毁对您来说并不重要。

而容器(basic_string在技术上不是C ++规范的容器)可以包含任意类型。具有析构函数的类型,或包含具有析构函数的对象的类型。这意味着,当对象被销毁时,对用户保持对的控制权更为重要。它明确指出:

  

a [移动对象]的所有现有元素都是移动分配或销毁。

所以差异确实有意义。一旦开始释放内存(通过分配器),就无法进行移动分配noexcept,因为这可能会因异常而失败。因此,一旦你开始要求在move-assign上释放内存,你就放弃了能够强制执行noexcept

答案 2 :(得分:0)

容器类中的移动赋值运算符被定义为noexcept,因为许多容器旨在实现强大的异常安全保证。容器实现强大的异常安全保证,因为在有移动分配操作符之前,容器必须被复制。如果副本出现任何问题,则删除新存储并保持容器不变。现在我们一直坚持这种行为。如果移动赋值op不是noexcept,则调用较慢的复制赋值运算符。