为什么容器移动赋值运算符不会超出?

Kno*_*abe 41 c++ c++11

我注意到std::string(真的std::basic_string是)移动赋值运算符是 noexcept.这对我来说很有意义.但后来我发现,没有一个标准集装箱(例如std::vector,std::deque,std::list,std::map)宣布其移动赋值操作符noexcept.这对我来说没什么意义.std::vector例如,A 通常实现为三个指针,并且指针当然可以移动分配而不会抛出异常.然后我想也许问题是移动容器的分配器,但std::string也有分配器,所以如果这是问题,我希望它会影响std::string.

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

How*_*ant 26

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

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

C& operator=(C&& c)
Run Code Online (Sandbox Code Playgroud)

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

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

如果 alloc_traits::propagate_on_container_move_assignment::valuefalseget_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).

总结如下:

C& operator=(C&& c)
        noexcept(
             alloc_traits::propagate_on_container_move_assignment::value &&
             is_nothrow_move_assignable<allocator_type>::value);
Run Code Online (Sandbox Code Playgroud)

我认为C::value_type在上述规范中没有任何依赖性,因此我认为它应该同样适用于std::basic_string尽管C++ 11另有说明.

更新

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

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

  1. 如果alloc_traits::propagate_on_container_move_assignment::valuetrue,那么规范现在需要移动分配allocator_typeto not throw exception(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{});
Run Code Online (Sandbox Code Playgroud)

并且,对于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

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


Nic*_*las 9

我认为这样做的理由是这样的.

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

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

的所有现有的元件一个 [被移动到对象]被上移分配到或销毁.

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