使用不可复制(但可移动)键移动地图分配时出错



为什么这不起作用:

#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;

在 ideone 上看到整个错误消息。

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

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

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

顺便说一句: g++ -v

gcc version 4.9.2 (Ubuntu 4.9.2-0ubuntu1~14.04) 
对我来说,

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

如果您进一步阅读表分配器感知容器要求,则同一行显示(对于a = rv(:

要求:如果allocator_traits<allocator_type>::propagate_on_container_move_assignment::value false,则T MoveInsertable XMoveAssignablea的所有现有元素要么被移动指定,要么被销毁。帖子:a应等于rv在此分配之前具有的值。

我想每个人都同意std::map<std::unique_ptr<char>, std::unique_ptr<int>>是一个分配器感知容器。 那么问题就变成了:对其移动分配运算符有什么要求?

如果我们只看分配器感知容器要求,那么只有在allocator_traits<allocator_type>::propagate_on_container_move_assignment::value false时才需要MoveInsertableMoveAssignable。 这比容器要求表所述的要求要弱,该表指出,无论分配器的属性如何,都必须MoveAssignable所有元素。 那么,分配器感知容器是否也必须满足容器的更严格要求?

让我们把它放开,如果标准没有那么努力地不重复自己,它应该说什么。

实施需要什么?

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

以下是allocator_traits<allocator_type>::propagate_on_container_move_assignment::value truemap赋值的libc++实现:

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

可以看出,绝对不需要key_typevalue_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 问题 2103 进行了此更改。

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

a = rv

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

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

在[map.overview]中指出:

满足容器所有要求的map

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

barmap=foo();

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

推理:

来自 §23.4.4.1

对于map<Key,T>,key_type是键,value_type是 pair<<strong>const 键,T>。

§ 23.2.3

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

7 关联容器满足分配器感知容器 (23.2.1( 的所有要求,但 对于映射和多重映射,表 95 中对value_type的要求适用于key_type 和mapped_type。[ 注意:例如,在某些情况下,key_type和mapped_type需要 可复制可分配,即使关联的value_type 对不是 可复制可分配。— 尾注 ]

从表 95:

表达:

a = RV

返回类型 :

X&

操作语义:

所有现有元素要么被指定移动,要么被销毁

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

a 应等于 RV 在此分配之前具有的值

复杂性:

线性

因此,您需要提供一个const Key&& move-assignment以使其可移植。

喜欢这个:

#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

我向任何认为答案无法接受的人道歉,但这些话确实在那里。

最新更新