通过移动分配具有不可复制(但可移动)键的映射的错误

时间:2016-04-07 11:57:44

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

为什么这不起作用:

#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的复制构造函数。我认为map赋值运算符使用新映射内容的移动赋值而不是旧映射内容,而std::swap不能这样做,因为它需要保持旧内容不变,所以它只是交换内部数据指针,因此它避免了问题。 / p>

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

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

顺便说一下:g++ -v

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

3 个答案:

答案 0 :(得分:5)

对我而言,这看起来像是C ++标准中规范的根本失败。规范在“不要重复自己”中走得太远,以至于变得难以理解和模棱两可(imho)。

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

  

要求:如果allocator_traits<allocator_type>::propagate_on_container_move_assignment::valuefalseT MoveInsertableXMoveAssignablea的所有现有元素都被移动分配或销毁。帖子:a应该等于rv在此作业之前的值。

我认为每个人都同意std::map<std::unique_ptr<char>, std::unique_ptr<int>>是一个支持分配器的容器。那么问题就变成了:它的移动赋值运算符有什么要求?

如果我们只查看可识别分配器的容器要求,则只有MoveInsertableMoveAssignable时才需要allocator_traits<allocator_type>::propagate_on_container_move_assignment::valuefalse。这比容器需求表所述的要求更弱,该表指出所有元素必须是MoveAssignable,而不管分配器的属性如何。那么分配器感知容器是否也必须满足容器的更严格要求?

让我们将其展开到标准 应该所说的内容,如果它没有那么努力不再重复。

实施需要什么?

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

mapallocator_traits<allocator_type>::propagate_on_container_move_assignment::value时,以下是true作业的libc ++实现:

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

可以看到绝对没有要求需要放在key_typevalue_type上。

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

这有什么用途?它会帮助或伤害std::map的客户吗?

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

我也相信C ++标准的当前规范风格是如此令人费解,甚至专家也无法就规范所说的内容达成一致。这不是因为专家是白痴。这是因为制定正确,明确的规范(按此规模)确实是一个非常困难的问题。

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

最后一个复杂因素:在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做出了这一改变。

答案 1 :(得分:1)

barmap=foo();

允许移动分配到地图的value_type

推理:

来自§23.4.4.1

  

对于map<Key,T>,key_type为Key,value_type为pair&lt; const   键下,T&GT;

§23.2.3

  

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

     

7关联容器满足Allocator感知容器的所有要求(23.2.1),除此之外   对于map和multimap,表95中对value_type的要求适用于key_type   和mapped_type。 [注意:例如,在某些情况下,需要使用key_type和mapped_type   即使关联的value_type,pair,也不是CopyAssignable   CopyAssignable。 - 结束说明]

从表95:

  

表达:

     

a = rv

     

返回类型:

     

X&安培;

     

操作语义:

     

a的所有现有元素都移动分配到或销毁

     

断言/注意前/后条件:

     

a应等于rv在此赋值之前的值

     

复杂度:

     

线性

所以你需要提供一个const Key&amp;&amp;移动分配使其便携。

像这样:

#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

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

答案 2 :(得分:1)

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

a = rv

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

  

a的所有现有元素都被移动分配或销毁

[map.overview]中说明:

  

map满足容器的所有要求

其中一项要求是移动分配。现在显然libstdc ++的方法是移动赋值元素,即使Key是不可复制的(这会使pair<const Key, T>不可移动 - 请注意它只是Key的不可复制性相关的)。但是没有授权移动任务发生,它只是一个选择。请注意,使用libc ++可以很好地编译代码。