在地图中使用 unique_ptr 时删除 std::pair 中的函数

Deleted Function in std::pair when using a unique_ptr inside a map

我有一段C++代码,我不确定它是否正确。考虑以下代码。

#include <memory>
#include <vector>
#include <map>

using namespace std;

int main(int argc, char* argv[])
{
    vector<map<int, unique_ptr<int>>> v;
    v.resize(5);

    return EXIT_SUCCESS;
}

GCC 编译这段代码没有问题。然而,英特尔编译器(版本 19)因错误而停止:

/usr/local/ [...] /include/c++/7.3.0/ext/new_allocator.h(136): error: function "std::pair<_T1, _T2>::pair(const std::pair<_T1, _T2> &) [with _T1=const int, _T2=std::unique_ptr<int, std::default_delete<int>>]" (declared at line 292 of "/usr/local/ [...] /include/c++/7.3.0/bits/stl_pair.h") cannot be referenced -- it is a deleted function
    { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
                            ^
      detected during:

[...]

instantiation of "void std::vector<_Tp, _Alloc>::resize(std::vector<_Tp, _Alloc>::size_type={std::size_t={unsigned long}}) [with _Tp=std::map<int, std::unique_ptr<int, std::default_delete<int>>, std::less<int>, std::allocator<std::pair<const int, std::unique_ptr<int, std::default_delete<int>>>>>, _Alloc=std::allocator<std::map<int, std::unique_ptr<int, std::default_delete<int>>, std::less<int>, std::allocator<std::pair<const int, std::unique_ptr<int, std::default_delete<int>>>>>>]"
                  at line 10 of "program.cpp"

两个编译器都可以毫无问题地编译以下代码。

#include <memory>
#include <vector>
#include <map>

using namespace std;

int main(int argc, char* argv[])
{
    vector<unique_ptr<int>> v;
    v.resize(5);

    return EXIT_SUCCESS;
}

第一个代码在 Intel 编译器中失败,因为它试图创建 unique_ptr 的副本,它只定义了一个移动构造函数。但是,我不确定第一个程序是否是合法的C++程序。

我想知道是不是第一个代码有错,还是Intel编译器有bug。如果第一个代码是错误的,为什么第二个是正确的?还是第二个也错了?

问题源于以下 post-std::vector<T>::resize[vector.capacity] 的条件:

Remarks: If an exception is thrown other than by the move constructor of a non-CopyInsertable T there are no effects.

也就是说,如果重定位失败,向量必须保持不变。重定位可能失败的原因之一是异常,具体来说,当用于将元素从旧存储转移到新存储的复制或移动构造函数抛出异常时。

复制 元素是否会以任何方式改变原始存储?没有1moving 元素会改变原来的存储吗?是的。哪种操作效率更高?移动。矢量总是喜欢移动而不是复制吗?不总是。

如果移动构造函数可以抛出异常,则不可能恢复旧存储的原始内容,因为尝试将已经移动的元素移回旧块可能会再次失败。在这种情况下,如果该移动构造函数保证它不会抛出异常(或移动当复制构造函数不可用时,构造函数是唯一的选择)。一个函数如何保证它不会抛出异常?一个将使用 noexcept 说明符进行注释并使用 noexcept 运算符进行测试。

正在使用 icc 测试以下代码:

std::map<int, std::unique_ptr<int>> m;
static_assert(noexcept(std::map<int, std::unique_ptr<int>>(std::move(m))), "!");

断言失败。这意味着 mnot nothrow-MoveConstructible.

标准要求是noexcept吗? [map.overview]:

// [map.cons], construct/copy/destroy:
map(const map& x);
map(map&& x);

std::map 既是 Move- 又是 CopyConstructible。两者都不需要不抛出异常。

但是,允许实现提供此保证{{citation needed}}。您的代码使用以下定义:

map(map&&) = default;

隐式生成的移动构造函数是否需要 noexcept[except.spec]:

An inheriting constructor ([class.inhctor]) and an implicitly declared special member function (Clause [special]) have an exception-specification. If f is an inheriting constructor or an implicitly declared default constructor, copy constructor, move constructor, destructor, copy assignment operator, or move assignment operator, its implicit exception-specification specifies the type-id T if and only if T is allowed by the exception-specification of a function directly invoked by f's implicit definition; f allows all exceptions if any function it directly invokes allows all exceptions, and f has the exception-specification noexcept(true) if every function it directly invokes allows no exceptions.

至此,icc move构造函数隐式生成的是不是noexcept就不好说了。无论哪种方式,std::map 本身都不需要是 nothrow-MoveConstructible,所以它更像是一个实现质量问题(库的实现或隐式生成构造函数的实现)和不管这是否是一个真正的错误,icc 都逃脱了。

最终,std::vector 将退回到使用更安全的选项,即复制构造函数来重新定位其元素(唯一指针的映射),但由于 std::unique_ptr 不是 CopyConstructible,报错

另一方面,std::unique_ptr的移动构造函数需要noexcept[unique.ptr.single.ctor]:

unique_ptr(unique_ptr&& u) noexcept;

唯一指针向量可以在需要重定位时安全地移动其元素。


在较新版本的 stl_map.h 中,用户提供了以下地图移动构造函数的定义:

map(map&& __x)
  noexcept(is_nothrow_copy_constructible<_Compare>::value)
  : _M_t(std::move(__x._M_t)) { }

这明确地使 noexcept 仅依赖于复制比较器是否抛出。


1 从技术上讲,接受非常量左值引用的复制构造函数可以更改原始对象,例如 std::auto_ptr,但是MoveInsertable 要求矢量元素可以从右值构造,不能绑定到非常量左值引用。