使用不可复制(但可移动)键移动分配地图时出错
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>>
是一个分配器感知容器。那么问题就变成了:它的移动赋值运算符有什么要求?
如果我们只看 分配器感知容器要求,那么 MoveInsertable
和 MoveAssignable
只有在 allocator_traits<allocator_type>::propagate_on_container_move_assignment::value
是 false
。这是一个比 Container requirements table 更弱的要求,其中规定 all 元素必须 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
为true
时map
赋值的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 进行了此更改。
为什么这不起作用:
#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
isfalse
,T
isMoveInsertable
intoX
andMoveAssignable
. All existing elements ofa
are either move assigned to or destroyed. post:a
shall be equal to the value thatrv
had before this assignment.
我想每个人都会同意 std::map<std::unique_ptr<char>, std::unique_ptr<int>>
是一个分配器感知容器。那么问题就变成了:它的移动赋值运算符有什么要求?
如果我们只看 分配器感知容器要求,那么 MoveInsertable
和 MoveAssignable
只有在 allocator_traits<allocator_type>::propagate_on_container_move_assignment::value
是 false
。这是一个比 Container requirements table 更弱的要求,其中规定 all 元素必须 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
为true
时map
赋值的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 进行了此更改。