42

我注意到std::string's (really std::basic_string's) 移动赋值运算符是 noexcept. 这对我来说很有意义。但后来我注意到没有一个标准容器(例如 , std::vector, std::deque, std::liststd::map声明它的移动赋值运算符noexcept。这对我来说意义不大。例如, Astd::vector通常实现为三个指针,并且指针当然可以移动分配而不会引发异常。然后我认为问题可能在于移动容器的分配器,但是std::string' 也有分配器,所以如果这是问题,我希望它会影响std::string.

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

4

3 回答 3

26

我相信我们正在研究标准缺陷。如果将noexcept规范应用于移动赋值运算符,则该规范有些复杂。而且我相信这个陈述是真实的,无论我们是在谈论basic_string还是vector

基于 [container.requirements.general]/p7 我对容器移动赋值运算符应该做的事情的英文翻译是:

C& operator=(C&& c)

如果alloc_traits::propagate_on_container_move_assignment::valuetrue,则转储资源,移动分配分配器,并从 转移资源c

如果 alloc_traits::propagate_on_container_move_assignment::valuefalse and get_allocator() == c.get_allocator(),则转储资源,并从 传输资源c

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

笔记:

  1. alloc_traitsallocator_traits<allocator_type>.

  2. 何时alloc_traits::propagate_on_container_move_assignment::value可以true指定移动赋值运算符,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).

总而言之:

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 另有规定。

更新

在下面的评论中,哥伦布正确地指出,事情一直在逐渐发生变化。我上面的评论是相对于 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 点不能抛出,因为可以转移资源。

所以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规范的变体。

如果您关心这个问题,最安全的做法是测试您关心的容器的明确专业化。我在container<T>这里为 VS、libstdc++ 和 libc++ 做了这些:

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

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

于 2012-09-08T22:15:47.323 回答
9

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

basic_string仅适用于非数组 POD 类型。因此,它们的析构函数必须是微不足道的。这意味着如果您执行swapfor 移动分配,那么移动到的字符串的原始内容还没有被破坏对您来说并不重要。

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

[移动到的对象]的所有现有元素要么被移动分配到要么被销毁。

所以这个区别是有道理的。noexcept一旦开始释放内存(通过分配器),就不能进行移动分配,因为这可能会因异常而失败。因此,一旦您开始要求在移动分配时释放内存,您就放弃了强制执行noexcept.

于 2012-09-08T17:33:33.687 回答
0

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

于 2018-01-08T02:54:10.217 回答