我注意到std::string
的(真正std::basic_string
)移动赋值运算符为noexcept
。这对我来说很有意义。但后来我发现没有标准容器(例如std::vector
,std::deque
,std::list
,std::map
)声明其移动赋值运算符noexcept
。这对我来说没什么意义。例如,std::vector
通常实现为三个指针,指针当然可以移动分配而不会抛出异常。然后我想也许问题是移动容器的分配器,但std::string
也有分配器,所以如果这是问题,我希望它会影响std::string
。
那么为什么std::string
移动赋值运算符noexcept
,但标准容器的移动赋值运算符不是?
答案 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::value
为false
和get_allocator() == c.get_allocator()
,转储资源和转移 来自c
的资源。如果
alloc_traits::propagate_on_container_move_assignment::value
为false
和get_allocator() != c.get_allocator()
,移动分配每个c[i]
。
注意:
alloc_traits
是指allocator_traits<allocator_type>
。
当alloc_traits::propagate_on_container_move_assignment::value
为true
时,可以指定移动分配运算符noexcept
,因为它将要释放当前资源,然后从源中窃取资源。同样在这种情况下,分配器也必须移动分配,并且移动分配必须为noexcept
,容器的移动分配必须为noexcept
。
当alloc_traits::propagate_on_container_move_assignment::value
为false
时,如果两个分配器相等,那么它将与#2做同样的事情。但是,在运行时之前,人们不知道分配器是否相等,因此您不能以此noexcept
为基础。
当alloc_traits::propagate_on_container_move_assignment::value
为false
时,如果两个分配器不相等,则必须移动分配每个单独的元素。这可能涉及向目标添加容量或节点,因此本质上是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另有规定。
<强>更新强>
在下面的评论中,Columbo正确地指出事情一直在变化。我上面的评论与C ++ 11相关。
对于草案C ++ 17(此时看起来似乎很稳定),情况有所改变:
如果alloc_traits::propagate_on_container_move_assignment::value
为true
,则规范现在要求allocator_type
的移动分配不抛出异常(17.6.3.5 [allocator.requirements] / p4)。因此,不再需要检查is_nothrow_move_assignable<allocator_type>::value
。
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草案中,vector
和string
移动赋值完全此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,则调用较慢的复制赋值运算符。