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

Posted

技术标签:

【中文标题】在地图中使用 unique_ptr 时 std::pair 中的已删除函数【英文标题】:Deleted Function in std::pair when using a unique_ptr inside a map 【发布时间】:2018-11-28 20:36:03 【问题描述】:

我有一段 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;

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

我想知道第一个代码是否错误或英特尔编译器是否存在错误。如果第一个代码是错误的,为什么第二个代码是正确的?还是第二个也错了?

【问题讨论】:

在我看来像一个英特尔漏洞。 英特尔很可能没有将pair 的移动构造函数标记为noexcept。如果这是一个 QOI 问题或实际上是一个错误,我可以回忆一下。 @NathanOliver 英特尔使用 GCC 的 STL(它没有),所以它会使用 GCC 可用的任何东西 @SergeyA 但 noexceptstd::is_nothrow_move_constructible 需要编译器支持,而且它们似乎无法在 ICC 上映射唯一指针 MSVC 18.00.40629(从2017年15.9.2开始)也有这个问题 【参考方案1】:

问题源于std::vector&lt;T&gt;::resize,[vector.capacity]的以下后置条件:

备注:如果非CopyInsertableT的移动构造函数抛出异常,则没有任何影响。

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

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

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

用icc测试下面的代码:

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

断言失败。这意味着m 不是 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]:

继承构造函数([class.inhctor])和隐式声明的特殊成员函数(子句 [special])具有异常规范。如果f 是继承构造函数或隐式声明的默认构造函数、复制构造函数、移动构造函数、析构函数、复制赋值运算符或移动赋值运算符,则其隐式异常规范 指定type-id T 当且仅当Tf 的隐式定义直接调用的函数的异常规范 允许; f 允许所有异常,如果它直接调用的任何函数允许所有异常,并且f 具有异常规范 noexcept(true) 如果它直接调用的每个函数都不允许异常。强>

此时,很难说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要求向量元素可以从 r 值构造,不能绑定到非 const 左值引用。

【讨论】:

很好的答案。谢谢。 重要的是要注意为什么尽管存在这种不当行为,但仍以这种方式指定库:主要原因是在容器中支持 incomplete 类型,例如 @ 987654353@,如果它们的元素类型是,则排除它们不可复制。

以上是关于在地图中使用 unique_ptr 时 std::pair 中的已删除函数的主要内容,如果未能解决你的问题,请参考以下文章

双向链表 std::unique_ptr 类在节点删除时无法按预期工作

在使用 std::unique_ptr 在退出作用域块时自动释放内存的情况下,为啥不直接使用堆栈呢?

使用 std::unique_ptr 的 C++ Pimpl Idiom 不完整类型

std::unique_ptr::get() 的奇怪返回行为

如何在 C++ 中使用 std::unique_ptr?

如何在构造函数中使用删除器初始化 std::unique_ptr?