【问题标题】:error by move assignment of map with non-copyable (but movable) key使用不可复制(但可移动)键移动地图分配错误
【发布时间】:2016-07-28 07:04:03
【问题描述】:

为什么这不起作用:

#include <memory>
#include <map>

std::map<std::unique_ptr<char>, std::unique_ptr<int>> foo();
std::map<std::unique_ptr<char>, std::unique_ptr<int>> barmap;

int main(){
  barmap=foo();
  return 0;
}

虽然这样做:

#include <memory>
#include <map>

std::map<std::unique_ptr<char>, std::unique_ptr<int>> foo();
std::map<std::unique_ptr<char>, std::unique_ptr<int>> barmap;

int main(){

  std::map<std::unique_ptr<char>, std::unique_ptr<int>> tmp(foo());
  using std::swap;
  swap(barmap, tmp);
  return 0;
}

这与映射中的键类型不可复制的事实有关(std::map 需要吗?)。使用g++ -std=c++14编译时的相关错误行:

/usr/include/c++/4.9/ext/new_allocator.h:120:4: error: use of deleted function ‘constexpr std::pair<_T1, _T2>::pair(std::pair<_T1, _T2>&&) [with _T1 = const std::unique_ptr<char>; _T2 = std::unique_ptr<int>]’
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^
In file included from /usr/include/c++/4.9/bits/stl_algobase.h:64:0,
                 from /usr/include/c++/4.9/memory:62,
                 from pairMove.cpp:1:
/usr/include/c++/4.9/bits/stl_pair.h:128:17: note: ‘constexpr std::pair<_T1, _T2>::pair(std::pair<_T1, _T2>&&) [with _T1 = const std::unique_ptr<char>; _T2 = std::unique_ptr<int>]’ is implicitly deleted because the default definition would be ill-formed:
       constexpr pair(pair&&) = default;
                 ^
/usr/include/c++/4.9/bits/stl_pair.h:128:17: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = char; _Dp = std::default_delete<char>]’
In file included from /usr/include/c++/4.9/memory:81:0,
                 from pairMove.cpp:1:
/usr/include/c++/4.9/bits/unique_ptr.h:356:7: note: declared here
       unique_ptr(const unique_ptr&) = delete;

完整的错误信息可见at ideone

在我看来,std::pair 的默认移动构造函数尝试使用 std::unique_ptr 的复制构造函数。我假设地图赋值运算符使用新地图内容的移动分配而不是旧地图内容,而std::swap 不能这样做,因为它需要保持旧内容完整,所以它只是交换内部数据指针,因此它避免了问题。

移动分配的必要性(至少能够)可能来自 C++11 中的 problemsallocator_traits&lt;M::allocator_type&gt;::propagate_on_container_move_assignment,但我的印象是在 C++14 中整个事情都已修复。我不确定为什么 STL 会选择移动赋值元素,而不是仅仅在移动赋值运算符中的容器之间交换数据指针。

以上所有内容都不能解释为什么移动地图中包含的对的移动分配失败 - 恕我直言,它不应该。

顺便说一句:g++ -v:

gcc version 4.9.2 (Ubuntu 4.9.2-0ubuntu1~14.04) 

【问题讨论】:

  • 嗯,我认为你的情况有所改变。您说第二个代码块有效,但随后您继续显示它的编译器错误。第一个代码块中也没有tmp。第二个示例中也没有定义MyPtr
  • @NathanOliver 对不起,我的错。两个代码实际上都有错误 - 现在已修复。 ideone.com 的错误实际上来自一个略有不同但相当于第一个块的代码。
  • 这两个代码显然都无法编译,因为 foo 没有定义,但如果我走这么远尝试链接它,那么我的问题已经解决了。我省略了foo 的定义,因为我不想让编译器有机会优化太多并消除问题。
  • 我通读了 C++14 中的 23.2.1,我认为第一个代码是否应该工作,但这是一个相当复杂的部分,所以我可能忽略了一些东西
  • 代码有地图的右值引用,而不是对

标签: c++ c++14 move-semantics stdmap


【解决方案1】:

在我看来,这似乎是 C++ 标准规范的根本失败。该规范在“不要重复自己”方面走得太远,以至于变得不可读和模棱两可(恕我直言)。

如果您进一步阅读表可识别分配器的容器要求,则同一行显示(a = rv):

要求:如果 allocator_traits&lt;allocator_type&gt;::propagate_on_container_move_assignment::valuefalseTMoveInsertableXMoveAssignablea 的所有现有元素要么被移动分配,要么被销毁。 post: a 应等于 rv 在此分配之前的值。

我想每个人都同意std::map&lt;std::unique_ptr&lt;char&gt;, std::unique_ptr&lt;int&gt;&gt; 是一个分配器感知容器。那么问题就变成了:它的移动赋值运算符有什么要求?

如果我们只看分配器感知容器要求,那么只有当allocator_traits&lt;allocator_type&gt;::propagate_on_container_move_assignment::valuefalse 时才需要MoveInsertableMoveAssignable。这是一个比 Container requirements 表中规定的要求更弱的要求,该表规定 所有 元素必须是 MoveAssignable,而不管分配器的属性如何。那么allocator-aware容器也必须满足容器更严格的要求吗?

让我们将其展开为标准应该所说的,如果它不努力不重复自己的话。

实施需要什么?

如果allocator_traits&lt;allocator_type&gt;::propagate_on_container_move_assignment::valuetrue,则内存资源的所有所有权都可以在移动分配期间从rhs 转移到lhs。这意味着map 移动分配只能做 O(1) 指针旋转以完成移动分配(当内存所有权可以转移时)。指针旋转不需要对指针指向的对象进行任何操作。

这是allocator_traits&lt;allocator_type&gt;::propagate_on_container_move_assignment::valuetruemap赋值的libc++实现:

https://github.com/llvm-mirror/libcxx/blob/master/include/__tree#L1531-L1551

可以看出完全没有要求需要放在key_type或者value_type上。

我们是否应该人为地对这些类型提出要求?

这样做有什么目的?对std::map的客户有帮助还是有伤害?

我个人的看法是,对不需要的客户类型提出要求只会让客户感到沮丧。

我还认为,当前 C++ 标准的规范风格非常复杂,以至于即使专家也无法就规范的内容达成一致。这不是因为专家是白痴。这是因为制定一个正确、明确的规范(在这种规模上)确实是一个非常困难的问题。

最后我相信,当规范冲突出现时,其意图是(或应该是)分配器感知容器需求取代容器需求。

最后一个并发症:在 C++11 中:

allocator_traits<allocator<T>>::propagate_on_container_move_assignment{} is false_type

如在 C++14 中:

allocator_traits<allocator<T>>::propagate_on_container_move_assignment{} is true_type

所以 libstdc++ 行为符合 C++11,而 libc++ 行为符合 C++14。 LWG issue 2103 进行了此更改。

【讨论】:

    【解决方案2】:

    我相信这是 libstdc++ 中的一个 错误 质量问题。如果我们查看容器要求表(现为table 100),其中一项要求是:

    a = rv
    

    其中aX(容器类)类型的值,rv 表示X 类型的非常量右值。操作语义描述为:

    a 的所有现有元素要么被移动分配,要么被销毁

    [map.overview] 中声明:

    map 满足容器的所有要求

    其中一个要求是移动分配。现在显然 libstdc++ 的方法是移动分配元素,即使在 Key 不可复制的情况下(这将使 pair&lt;const Key, T&gt; 不可移动 - 请注意,这里只有 Key 的不可复制性相关)。但是没有强制要求移动分配发生,它只是一种选择。请注意,代码使用 libc++ 编译得很好。

    【讨论】:

    • 来自链接中的表 100,在引入 a = rv 的表的行上,它表示语义是“a 的所有现有元素要么被分配到要么被销毁”。您不能移动分配 pair 除非 K 有一个移动分配运算符,该运算符在右侧采用 const K&& 。我认为 libstc++ 是符合标准的,libc++ 超出了对标准中短语的迂腐解释。我的观点是标准的措辞是错误的。它可能应该说“如果不可移动分配将被销毁,否则可能被移动分配给”。
    • @RichardHodges 这种解释意味着关联容器永远不能移动构造或移动分配。我不认为元素移动要求确实需要移动value_types。
    • 不是这样,因为当键是可复制的(几乎总是如此)时,const K&& 可以绑定到复制构造函数的 const K&。
    • 我并不是说实现实际上是这样做的,但允许存在要求,因为标准(恕我直言,错误地)要求它。
    • 不,libstdc++ 的问题是它根本无法进行标记调度(或类似的操作),所以未采用的分支仍在编译中。
    【解决方案3】:
    barmap=foo();
    

    允许要求将移动分配到地图的value_type

    推理:

    来自§23.4.4.1

    对于map&lt;Key,T&gt;,key_type 是 Key,value_type 是 pairconst 键,T>.

    § 23.2.3

    5 对于 set 和 multiset,值类型与键类型相同。对于地图和多地图,它等于pair&lt;const Key, T&gt;

    7 关联容器满足分配器感知容器 (23.2.1) 的所有要求,除了 对于 map 和 multimap,表 95 中对 value_type 的要求适用于 key_type 和映射类型。 [注:例如,在某些情况下,key_type 和 mapped_type 需要是 即使关联的 value_type、pair 不是 CopyAssignable 可复制。 ——尾注]

    从表 95:

    表达式:

    a = rv

    返回类型:

    X&

    操作语义:

    a 的所有现有元素要么被分配到要么被销毁

    断言/注释前置/后置条件:

    a 应等于 rv 在此分配之前的值

    复杂性:

    线性

    因此您需要提供一个 const Key&& 移动分配以使其可移植。

    像这样:

    #include <memory>
    #include <map>
    
    struct key {
    
      key(key&&);
      key(const key&&);
      key& operator=(key&&);
      key& operator=(const key&&);
    };
    bool operator<(const key& l, const key& r);
    
    struct value {
    
    };
    
    using map_type = std::map<key, value>;
    
    map_type foo();
    map_type foo2();
    
    int main(){
      auto barmap=foo();
      barmap = foo2();
      return 0;
    }
    

    在这里编译:https://godbolt.org/g/XAQxjt

    链接到我使用的 2015 草案标准(我知道有一个更高版本,但该行保留在最新草案中,现在在表 100 中)

    http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4527.pdf

    我向任何发现答案不可接受的人道歉,但这些话确实存在。

    【讨论】:

    • 这个副本分配情况如何?从foo 产生的值是一个右值,所以它应该使用移动赋值。使用 auto 会导致这是移动构造 - 我知道它可以工作,但我需要的是移动分配。
    • 这是一个复制/移动上下文。 foo() 是纯右值,所以实际上是移动赋值
    • @j_kubik 我已经更新了答案,以防我不够清楚。
    • @M.M 除了const unique_ptr&lt;&gt;&amp;&amp; 没有移动赋值或移动构造函数...查看更新的答案。
    • @M.M 说得好。我在 iso c++ 邮件列表中提出了这个问题,希望澄清它是否是标准中的缺陷,或者我是否缺少一些基本的东西。
    猜你喜欢
    • 1970-01-01
    • 2014-12-15
    • 1970-01-01
    • 1970-01-01
    • 2016-01-10
    • 1970-01-01
    • 2012-12-28
    • 2015-11-17
    • 2023-03-07
    相关资源
    最近更新 更多