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

error by move assignment of map with non-copyable (but movable) key

为什么这不起作用:

#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 不能这样做,因为它需要保持旧内容完整,所以它只是交换内部数据指针,因此它避免了问题。

(至少能够)移动赋值的必要性可能来自 problems 和 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) 
barmap=foo();

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

推理:

来自 §23.4.4.1

For a map<Key,T> the key_type is Key and the value_type is pair<const Key,T>.

§ 23.2.3

5 For set and multiset the value type is the same as the key type. For map and multimap it is equal to pair<const Key, T>.

7 The associative containers meet all the requirements of Allocator-aware containers (23.2.1), except that for map and multimap, the requirements placed on value_type in Table 95 apply instead to key_type and mapped_type. [ Note: For example, in some cases key_type and mapped_type are required to be CopyAssignable even though the associated value_type, pair, is not CopyAssignable. — end note ]

来自 Table 95:

Expression :

a = rv

Return type :

X&

Operational semantics:

All existing elements of a are either move assigned to or destroyed

Assertion/note pre-/post-condition:

a shall be equal to the value that rv had before this assignment

Complexity:

linear

因此您需要提供一个 const Key&& 移动赋值以使其成为 table.

像这样:

#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

link to 2015 draft standard I have used (I know there is a later one, but the line made in the most recent draft, now in table100)

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

我向任何发现答案未被接受的人道歉table,但文字确实存在。

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

a = rv

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

All existing elements of a are either move assigned to or destroyed

[map.overview]中指出:

A map satisfies all of the requirements of a container

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

对我来说,这看起来像是 C++ 标准中规范的根本失败。该规范在 "do not repeat yourself" 中走得太远,以至于变得不可读和模棱两可(恕我直言)。

如果您进一步阅读 table 分配器感知容器要求,同一行说(对于 a = rv):

Requires: If allocator_traits<allocator_type>::propagate_on_container_move_assignment::value is false, T is MoveInsertable into X and MoveAssignable. All existing elements of a are either move assigned to or destroyed. post: a shall be equal to the value that rv had before this assignment.

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

如果我们只看 分配器感知容器要求,那么 MoveInsertableMoveAssignable 只有在 allocator_traits<allocator_type>::propagate_on_container_move_assignment::valuefalse。这是一个比 Container requirements table 更弱的要求,其中规定 all 元素必须 MoveAssignable 无论如何分配器的属性。那么分配器感知容器也必须满足容器更严格的要求吗?

让我们把它展开到标准 应该 说的,如果它不是那么努力不重复自己的话。

实施需要什么?

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

这里是当allocator_traits<allocator_type>::propagate_on_container_move_assignment::valuetruemap赋值的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 issue 2103 进行了此更改。